From af23eec6828951196b44207257ab318fd6f749ce Mon Sep 17 00:00:00 2001 From: Kate Callon <70660419+kcallon@users.noreply.github.com> Date: Thu, 29 Feb 2024 12:51:06 -0800 Subject: [PATCH 1/3] Updated Medications View + Adding to Environment Variable (#51) # Working Medications View with Sample Patient Data and Updating Global Environment Variable ## :recycle: Current situation & Problem #21 Previously, the medications view would only pull the display name from the medications fhir resource. This was problematic in several ways--the UI was not developed, patient medications could be added that weren't accurate, and there was no additional information beyond the display name. ## :gear: Release Notes - Updated the patient medication data to be filtered by only including outpatient and activate medications (like from llmonfhir) - Modified existing code from SpeziMedications to create structs specific to Intake such as IntakeMedications and IntakeDosage - Updated medicationOptions to represent some of the top mock patient medications - Updated a MedicationSettingsViewModel to initialize with existing mock patient medications including dosage, intake method, and dose frequency - Added compatibility to global variable ## :books: Documentation *Please ensure that you properly document any additions in conformance to [Spezi Documentation Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).* *You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.* ## :white_check_mark: Testing *Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.* *This section describes important information about the tests and why some elements might not be testable.* ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [x ] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --- Intake.xcodeproj/project.pbxproj | 49 ++++++- .../xcshareddata/swiftpm/Package.resolved | 11 +- Intake/Home.swift | 2 +- Intake/Intake.swift | 2 +- Intake/Medication View/IntakeDosage.swift | 20 +++ Intake/Medication View/IntakeMedication.swift | 21 +++ .../IntakeMedicationInstance.swift | 30 ++++ .../IntakeMedicationViewModel.swift | 133 ++++++++++++++++++ .../MedicationContentView.swift | 51 +++++++ Intake/MedicationView.swift | 102 -------------- Intake/Mock Data/FHIRStore+Extensions.swift | 101 ++++--------- Intake/Resources/Localizable.xcstrings | 11 +- 12 files changed, 342 insertions(+), 191 deletions(-) create mode 100644 Intake/Medication View/IntakeDosage.swift create mode 100644 Intake/Medication View/IntakeMedication.swift create mode 100644 Intake/Medication View/IntakeMedicationInstance.swift create mode 100644 Intake/Medication View/IntakeMedicationViewModel.swift create mode 100644 Intake/Medication View/MedicationContentView.swift delete mode 100644 Intake/MedicationView.swift diff --git a/Intake.xcodeproj/project.pbxproj b/Intake.xcodeproj/project.pbxproj index 6528a33..dd38b8e 100644 --- a/Intake.xcodeproj/project.pbxproj +++ b/Intake.xcodeproj/project.pbxproj @@ -58,7 +58,12 @@ 2FF53D8B2A8725DE00042B76 /* SpeziMockWebService in Frameworks */ = {isa = PBXBuildFile; productRef = 2FF53D8A2A8725DE00042B76 /* SpeziMockWebService */; }; 2FF53D8D2A8729D600042B76 /* IntakeStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */; }; 511827962B740192002033A0 /* SurgeryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511827952B740191002033A0 /* SurgeryView.swift */; }; - 511827982B7401A8002033A0 /* MedicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511827972B7401A8002033A0 /* MedicationView.swift */; }; + 51805C122B81853800D17109 /* IntakeMedication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C112B81853700D17109 /* IntakeMedication.swift */; }; + 51805C152B81857100D17109 /* IntakeMedicationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */; }; + 51805C182B81898700D17109 /* SpeziMedication in Frameworks */ = {isa = PBXBuildFile; productRef = 51805C172B81898700D17109 /* SpeziMedication */; }; + 51805C1A2B818A1A00D17109 /* IntakeDosage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C192B818A1A00D17109 /* IntakeDosage.swift */; }; + 51805C1D2B818A4400D17109 /* IntakeMedicationInstance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C1C2B818A4400D17109 /* IntakeMedicationInstance.swift */; }; + 51A027672B82CDA300A195C8 /* MedicationContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A027662B82CDA300A195C8 /* MedicationContentView.swift */; }; 5661551D2AB8384200209B80 /* SwiftPackageList in Frameworks */ = {isa = PBXBuildFile; productRef = 5661551C2AB8384200209B80 /* SwiftPackageList */; }; 566155292AB8447C00209B80 /* Package+LicenseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 566155282AB8447C00209B80 /* Package+LicenseType.swift */; }; 5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5661552D2AB854C000209B80 /* PackageHelper.swift */; }; @@ -164,7 +169,11 @@ 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = ""; }; 2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntakeStandard.swift; sourceTree = ""; }; 511827952B740191002033A0 /* SurgeryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurgeryView.swift; sourceTree = ""; }; - 511827972B7401A8002033A0 /* MedicationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MedicationView.swift; sourceTree = ""; }; + 51805C112B81853700D17109 /* IntakeMedication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedication.swift; sourceTree = ""; }; + 51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedicationViewModel.swift; sourceTree = ""; }; + 51805C192B818A1A00D17109 /* IntakeDosage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeDosage.swift; sourceTree = ""; }; + 51805C1C2B818A4400D17109 /* IntakeMedicationInstance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedicationInstance.swift; sourceTree = ""; }; + 51A027662B82CDA300A195C8 /* MedicationContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedicationContentView.swift; sourceTree = ""; }; 566155282AB8447C00209B80 /* Package+LicenseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Package+LicenseType.swift"; sourceTree = ""; }; 5661552D2AB854C000209B80 /* PackageHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageHelper.swift; sourceTree = ""; }; 5680DD382AB8983D004E6D4A /* PackageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageCell.swift; sourceTree = ""; }; @@ -222,6 +231,7 @@ F42AB1D42B6379B5002E13A6 /* SpeziLLMLocal in Frameworks */, 2FB099AF2A875DF100B20952 /* FirebaseAuth in Frameworks */, 97D73D6A2AD860AD00B47FA0 /* SpeziFirebaseStorage in Frameworks */, + 51805C182B81898700D17109 /* SpeziMedication in Frameworks */, F42AB1D82B6379B5002E13A6 /* SpeziLLMOpenAI in Frameworks */, 2FE5DC6729EDD894004B9AB4 /* SpeziContact in Frameworks */, 5A0C1A1B2B69691000120506 /* SpeziFHIRHealthKit in Frameworks */, @@ -363,6 +373,18 @@ path = Surgery; sourceTree = ""; }; + 519E830A2B7C4F1600A2D92D /* Medication View */ = { + isa = PBXGroup; + children = ( + 51805C112B81853700D17109 /* IntakeMedication.swift */, + 51805C192B818A1A00D17109 /* IntakeDosage.swift */, + 51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */, + 51805C1C2B818A4400D17109 /* IntakeMedicationInstance.swift */, + 51A027662B82CDA300A195C8 /* MedicationContentView.swift */, + ); + path = "Medication View"; + sourceTree = ""; + }; 56F6F29E2AB441640022FE5A /* Contributions */ = { isa = PBXGroup; children = ( @@ -457,7 +479,7 @@ isa = PBXGroup; children = ( F4F4F8802B8C6FC5008FBEED /* Elements.swift */, - 511827972B7401A8002033A0 /* MedicationView.swift */, + 519E830A2B7C4F1600A2D92D /* Medication View */, 511827942B740191002033A0 /* Surgery */, AC2A17272B70684D00F560D0 /* SocialHistory */, 5A2B9FB32B6AFE1F005CA63F /* Allergy Records */, @@ -594,6 +616,7 @@ 5A0C1A182B69691000120506 /* SpeziFHIR */, 5A0C1A1A2B69691000120506 /* SpeziFHIRHealthKit */, 5A2B9F892B69E0AF005CA63F /* SpeziFHIRMockPatients */, + 51805C172B81898700D17109 /* SpeziMedication */, ); productName = Intake; productReference = 653A254D283387FE005D4D48 /* Intake.app */; @@ -690,6 +713,7 @@ 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */, F42AB1D02B6379B5002E13A6 /* XCRemoteSwiftPackageReference "SpeziLLM" */, 5A0C1A162B69667B00120506 /* XCRemoteSwiftPackageReference "SpeziFHIR" */, + 51805C162B81898700D17109 /* XCRemoteSwiftPackageReference "SpeziMedication" */, ); productRefGroup = 653A254E283387FE005D4D48 /* Products */; projectDirPath = ""; @@ -782,12 +806,12 @@ 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */, 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */, + 51805C1D2B818A4400D17109 /* IntakeMedicationInstance.swift in Sources */, AC2A17292B70686000F560D0 /* SocialHistoryQuestions.swift in Sources */, A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* Intake.docc in Sources */, 2FF53D8D2A8729D600042B76 /* IntakeStandard.swift in Sources */, - 511827982B7401A8002033A0 /* MedicationView.swift in Sources */, 2FE5DC4729EDD7F2004B9AB4 /* CodableArray+RawRepresentable.swift in Sources */, 5A2B9FAB2B69E430005CA63F /* FHIRStore+Extensions.swift in Sources */, A9720E432ABB68CC00872D23 /* AccountSetupHeader.swift in Sources */, @@ -799,7 +823,9 @@ 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, 5A2B9F872B69E06B005CA63F /* ResourceSelection.swift in Sources */, 2F4E23832989D51F0013F3D9 /* IntakeTestingSetup.swift in Sources */, + 51805C152B81857100D17109 /* IntakeMedicationViewModel.swift in Sources */, 5A2B9F862B69E06B005CA63F /* SettingsView.swift in Sources */, + 51805C1A2B818A1A00D17109 /* IntakeDosage.swift in Sources */, F4F4F8812B8C6FC5008FBEED /* Elements.swift in Sources */, 2FE5DC5329EDD7FA004B9AB4 /* Bundle+Questionnaire.swift in Sources */, 5A2B9FB62B6AFE5D005CA63F /* AllergyRecords.swift in Sources */, @@ -815,7 +841,9 @@ 511827962B740192002033A0 /* SurgeryView.swift in Sources */, 2FE5DC5229EDD7FA004B9AB4 /* IntakeScheduler.swift in Sources */, F42AB1E52B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift in Sources */, + 51805C122B81853800D17109 /* IntakeMedication.swift in Sources */, A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */, + 51A027672B82CDA300A195C8 /* MedicationContentView.swift in Sources */, 653A2551283387FE005D4D48 /* Intake.swift in Sources */, 2FE5DC3629EDD7CA004B9AB4 /* HealthKitPermissions.swift in Sources */, 5661552E2AB854C000209B80 /* PackageHelper.swift in Sources */, @@ -1466,6 +1494,14 @@ minimumVersion = 1.0.0; }; }; + 51805C162B81898700D17109 /* XCRemoteSwiftPackageReference "SpeziMedication" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/StanfordSpezi/SpeziMedication.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.4.0; + }; + }; 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/FelixHerrmann/swift-package-list"; @@ -1605,6 +1641,11 @@ package = 2FE750CA2A87240100723EAE /* XCRemoteSwiftPackageReference "SpeziMockWebService" */; productName = SpeziMockWebService; }; + 51805C172B81898700D17109 /* SpeziMedication */ = { + isa = XCSwiftPackageProductDependency; + package = 51805C162B81898700D17109 /* XCRemoteSwiftPackageReference "SpeziMedication" */; + productName = SpeziMedication; + }; 5661551C2AB8384200209B80 /* SwiftPackageList */ = { isa = XCSwiftPackageProductDependency; package = 5661551B2AB8384200209B80 /* XCRemoteSwiftPackageReference "swift-package-list" */; diff --git a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4c73220..67a1a63 100644 --- a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -246,12 +246,21 @@ { "identity" : "spezillm", "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziLLM.git", + "location" : "https://github.com/StanfordSpezi/SpeziLLM", "state" : { "revision" : "6892c5dfe258371b6f3287f02b8fec57a611ba70", "version" : "0.7.0" } }, + { + "identity" : "spezimedication", + "kind" : "remoteSourceControl", + "location" : "https://github.com/StanfordSpezi/SpeziMedication.git", + "state" : { + "revision" : "95ca9aebbd23f3842639d6e322785a0ff3620aac", + "version" : "0.4.0" + } + }, { "identity" : "spezimockwebservice", "kind" : "remoteSourceControl", diff --git a/Intake/Home.swift b/Intake/Home.swift index 569cbcd..0d2fe2e 100644 --- a/Intake/Home.swift +++ b/Intake/Home.swift @@ -93,7 +93,7 @@ struct HomeView: View { case .surgical: SurgeryView() case .medical: MedicalHistoryView() case .social: SocialHistoryQuestionView() - case .medication: MedicationView() + case .medication: MedicationContentView() case .concern: SummaryView(chiefComplaint: $data.chiefComplaint) } } diff --git a/Intake/Intake.swift b/Intake/Intake.swift index 3f619e9..80c910c 100644 --- a/Intake/Intake.swift +++ b/Intake/Intake.swift @@ -19,7 +19,7 @@ class NavigationPathWrapper { class DataStore { var allergyData: [AllergyItem] = [] var conditionData: [MedicalHistoryItem] = [] - var medicationData: [MedicationItem] = [] + var medicationData: Set = [] var surgeries: [SurgeryItem] = [] var chiefComplaint: String = "" } diff --git a/Intake/Medication View/IntakeDosage.swift b/Intake/Medication View/IntakeDosage.swift new file mode 100644 index 0000000..8641810 --- /dev/null +++ b/Intake/Medication View/IntakeDosage.swift @@ -0,0 +1,20 @@ +// +// IntakeDosage.swift +// Intake +// +// Created by Kate Callon on 2/17/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Template Medication project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziMedication + +struct IntakeDosage: Dosage { + var localizedDescription: String +} diff --git a/Intake/Medication View/IntakeMedication.swift b/Intake/Medication View/IntakeMedication.swift new file mode 100644 index 0000000..b8ba5a1 --- /dev/null +++ b/Intake/Medication View/IntakeMedication.swift @@ -0,0 +1,21 @@ +// +// IntakeMedication.swift +// Intake +// +// Created by Kate Callon on 2/17/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Medication Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziMedication + +struct IntakeMedication: Medication, Comparable { + var localizedDescription: String + var dosages: [IntakeDosage] +} diff --git a/Intake/Medication View/IntakeMedicationInstance.swift b/Intake/Medication View/IntakeMedicationInstance.swift new file mode 100644 index 0000000..51f0140 --- /dev/null +++ b/Intake/Medication View/IntakeMedicationInstance.swift @@ -0,0 +1,30 @@ +// +// IntakeMedicationInstance.swift +// Intake +// +// Created by Kate Callon on 2/17/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Template Medication project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziMedication + +struct IntakeMedicationInstance: MedicationInstance, MedicationInstanceInitializable { + let id: UUID + let type: IntakeMedication + var dosage: IntakeDosage + var schedule: Schedule + + init(type: IntakeMedication, dosage: IntakeDosage, schedule: Schedule) { + self.id = UUID() + self.type = type + self.dosage = dosage + self.schedule = schedule + } +} diff --git a/Intake/Medication View/IntakeMedicationViewModel.swift b/Intake/Medication View/IntakeMedicationViewModel.swift new file mode 100644 index 0000000..acd28ca --- /dev/null +++ b/Intake/Medication View/IntakeMedicationViewModel.swift @@ -0,0 +1,133 @@ +// +// IntakeMedicationViewModel.swift +// Intake +// +// Created by Kate Callon on 2/17/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Template Medication project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import class ModelsR4.MedicationRequest +import Spezi +import SpeziFHIR +import SpeziMedication +import SwiftUI + +@Observable +class IntakeMedicationSettingsViewModel: Module, MedicationSettingsViewModel, CustomStringConvertible { + var medicationInstances: Set = [] + let medicationOptions: Set + + var description: String { + guard !medicationInstances.isEmpty else { + return "No Medications" + } + + return medicationInstances + .map { medicationInstance in + let scheduleDescription: String + switch medicationInstance.schedule.frequency { + case let .regularDayIntervals(dayInterval): + scheduleDescription = "RegularDayIntervals: \(dayInterval)" + case let .specificDaysOfWeek(weekdays): + scheduleDescription = "SpecificDaysOfWeek: \(weekdays)" + case .asNeeded: + scheduleDescription = "AsNeeded" + } + + return "\(medicationInstance.type.localizedDescription) - \(medicationInstance.dosage.localizedDescription) - \(scheduleDescription)" + } + .joined(separator: ", ") + } + + init(existingMedications: [FHIRResource]) { // swiftlint:disable:this function_body_length + self.medicationOptions = [ + IntakeMedication( + localizedDescription: "Hydrochlorothiazide 25 MG Oral Tablet", + dosages: [ + IntakeDosage(localizedDescription: "25 MG") + ] + ), + IntakeMedication( + localizedDescription: "Acetaminophen 160 MG Chewable Tablet", + dosages: [ + IntakeDosage(localizedDescription: "160 MG") + ] + ), + IntakeMedication( + localizedDescription: "Fexofenadine hydrochloride 30 MG Oral Tablet", + dosages: [ + IntakeDosage(localizedDescription: "30 MG") + ] + ), + IntakeMedication( + localizedDescription: "NDA020800 0.3 ML Epinephrine 1 MG/ML Auto-Injector", + dosages: [ + IntakeDosage(localizedDescription: "0.3ML / 1 MG/ML") + ] + ) + ] + + var foundMedications: [IntakeMedicationInstance] = [] + if !existingMedications.isEmpty { + for medication in existingMedications { + for option in medicationOptions where option.localizedDescription == medication.displayName { + var medSchedule: SpeziMedication.Schedule + let medRequest = medicationRequest(resource: medication) + if case .boolean(let asNeeded) = medRequest?.dosageInstruction?.first?.asNeeded { + if let asNeededbool = asNeeded.value?.bool { + if asNeededbool { + medSchedule = SpeziMedication.Schedule(frequency: .asNeeded) + } else { + let intValue: Int + let interval = medRequest?.dosageInstruction?.first?.timing?.repeat?.period?.value?.decimal + if let interval = interval { + intValue = interval.int + } else { + continue + } + medSchedule = Schedule(frequency: .regularDayIntervals(intValue)) + } + + guard let firstDosage = option.dosages.first else { + continue + } + + let intakeMedicationInstance = IntakeMedicationInstance( + type: option, + dosage: firstDosage, + schedule: medSchedule + ) + foundMedications.append(intakeMedicationInstance) + } + } + } + } + self.medicationInstances = Set(foundMedications) + } + } + func persist(medicationInstances: Set) async throws { + self.medicationInstances = medicationInstances + } + + func medicationRequest(resource: FHIRResource) -> MedicationRequest? { + guard case let .r4(resource) = resource.versionedResource, + let medicationRequest = resource as? ModelsR4.MedicationRequest else { + return nil + } + return medicationRequest + } +} + +extension Decimal { + var int: Int { + let intVal = NSDecimalNumber(decimal: self).intValue // swiftlint:disable:this legacy_objc_type + return intVal + } +} diff --git a/Intake/Medication View/MedicationContentView.swift b/Intake/Medication View/MedicationContentView.swift new file mode 100644 index 0000000..052c0fc --- /dev/null +++ b/Intake/Medication View/MedicationContentView.swift @@ -0,0 +1,51 @@ +// +// MedicationContentView.swift +// Intake +// +// Created by Kate Callon on 2/18/24. +// +// +// This source file is part of the Intake based on the Stanford Spezi Template Medication project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import SpeziFHIR +import SpeziMedication +import SwiftUI + +struct MedicationContentView: View { + @Environment(FHIRStore.self) private var fhirStore + @Environment(NavigationPathWrapper.self) private var navigationPath + @Environment(DataStore.self) private var data + @State private var presentSettings = false + + @State private var medicationSettingsViewModel: IntakeMedicationSettingsViewModel? + + var body: some View { + VStack { + if let medicationSettingsViewModel { + MedicationSettings(allowEmtpySave: true, medicationSettingsViewModel: medicationSettingsViewModel) { + data.medicationData = medicationSettingsViewModel.medicationInstances + navigationPath.path.append(NavigationViews.allergies) + } + .navigationTitle("Medication Settings") + } else { + ProgressView() + } + } + .task { + let patientMedications = fhirStore.llmMedications + self.medicationSettingsViewModel = IntakeMedicationSettingsViewModel(existingMedications: patientMedications) + } + } + + init() {} +} + +#Preview { + MedicationContentView() +} diff --git a/Intake/MedicationView.swift b/Intake/MedicationView.swift deleted file mode 100644 index 2efae8d..0000000 --- a/Intake/MedicationView.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// MedicationView.swift -// Intake -// -// Created by Kate Callon on 2/6/24. -// -// This source file is part of the Intake based on the Stanford Spezi Template Application project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import SpeziFHIR -import SwiftUI - -struct MedicationItem: Identifiable { - var id = UUID() - var medicationName: String -} - -struct MedicationView: View { - @Environment(FHIRStore.self) private var fhirStore - @Environment(NavigationPathWrapper.self) private var navigationPath - @State private var medications: [MedicationItem] = [] - - var body: some View { - NavigationView { // swiftlint:disable:this closure_body_length - VStack { // swiftlint:disable:this closure_body_length - List { - ForEach($medications) { $item in - HStack { - TextField("Medication", text: $item.medicationName) - Button(action: { - // Action to delete this item - if let index = medications.firstIndex(where: { $0.id == item.id }) { - medications.remove(at: index) - } - }) { - Image(systemName: "xmark.circle") - .accessibilityLabel(Text("DELETE_MEDICATION")) - } - } - } - .onDelete(perform: delete) - - Button(action: { - // Action to add new item - medications.append(MedicationItem(medicationName: "")) - }) { - HStack { - Image(systemName: "plus.circle.fill") - .accessibilityLabel(Text("ADD_MEDICATION")) - Text("Add Field") - } - } - } - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .principal) { - Text("Please list your current medications.") - .font(.system(size: 28)) // Choose a size that fits - .lineLimit(1) - .minimumScaleFactor(0.5) // Adjusts the font size to fit the width of the line - } - } - Button(action: { - // Save output to Firestore and navigate to next screen - // Still need to save output to Firestore - navigationPath.path.append(NavigationViews.allergies) - }) { - Text("Submit") - .foregroundColor(.white) - .padding() - .frame(maxWidth: .infinity) - .background(Color.blue) - .cornerRadius(8) - } - .padding() - } - .onAppear { - // Set a breakpoint on the next line to inspect `fhirStore.conditions` - let patientMedications = fhirStore.medications - for medication in patientMedications where !self.medications.contains(where: { $0.medicationName == medication.displayName }) { - self.medications.append(MedicationItem(medicationName: medication.displayName)) - } - } - } - } - - func delete(at offsets: IndexSet) { - medications.remove(atOffsets: offsets) - } - } - -#Preview { - MedicationView() - .previewWith { - FHIRStore() - } -} diff --git a/Intake/Mock Data/FHIRStore+Extensions.swift b/Intake/Mock Data/FHIRStore+Extensions.swift index fb90761..bf72109 100644 --- a/Intake/Mock Data/FHIRStore+Extensions.swift +++ b/Intake/Mock Data/FHIRStore+Extensions.swift @@ -63,8 +63,36 @@ extension FHIRStore { } ?? false } } + var llmMedications: [FHIRResource] { + let outpatientMedications = medications + .filter { medication in + guard let medicationRequest = medicationRequest(resource: medication), + medicationRequest.category? + .contains(where: { codableconcept in + codableconcept.text?.value?.string.lowercased() == "outpatient" + }) + ?? false else { + return false + } + + return true + } + .uniqueDisplayNames + + let activeMedications = medications + .filter { medication in + guard let medicationRequest = medicationRequest(resource: medication), + medicationRequest.status == .active else { + return false + } + + return true + } + .uniqueDisplayNames + + return outpatientMedications + activeMedications + } - private var llmMedications: [FHIRResource] { func medicationRequest(resource: FHIRResource) -> MedicationRequest? { guard case let .r4(resource) = resource.versionedResource, let medicationRequest = resource as? ModelsR4.MedicationRequest else { @@ -73,77 +101,6 @@ extension FHIRStore { return medicationRequest } - - let outpatientMedications = medications - .filter { medication in - guard let medicationRequest = medicationRequest(resource: medication), - medicationRequest.category? - .contains(where: { codableconcept in - codableconcept.text?.value?.string.lowercased() == "outpatient" - }) - ?? false else { - return false - } - - return true - } - .uniqueDisplayNames - - let activeMedications = medications - .filter { medication in - guard let medicationRequest = medicationRequest(resource: medication), - medicationRequest.status == .active else { - return false - } - - return true - } - .uniqueDisplayNames - - return outpatientMedications + activeMedications - } - -// var allResourcesFunctionCallIdentifier: [String] { -// @AppStorage(StorageKeys.resourceLimit) var resourceLimit = StorageKeys.Defaults.resourceLimit -// -// let relevantResources: [FHIRResource] -// -// if llmRelevantResources.count > resourceLimit { -// relevantResources = llmRelevantResources -// .lazy -// .filter { -// $0.date != nil -// } -// .sorted { -// $0.date ?? .distantPast < $1.date ?? .distantPast -// } -// .suffix(resourceLimit) -// } else { -// relevantResources = llmRelevantResources -// } -// -// return Array(Set(relevantResources.map { $0.functionCallIdentifier })) -// } -// -// -// func loadMockResources() { -// if FeatureFlags.testMode { -// let mockObservation = Observation( -// code: CodeableConcept(coding: [Coding(code: "1234".asFHIRStringPrimitive())]), -// id: FHIRPrimitive("1234"), -// issued: FHIRPrimitive(try? Instant(date: .now)), -// status: FHIRPrimitive(ObservationStatus.final) -// ) -// -// let mockFHIRResource = FHIRResource( -// versionedResource: .r4(mockObservation), -// displayName: "Mock Resource" -// ) -// -// removeAllResources() -// insert(resource: mockFHIRResource) -// } -// } } extension Array where Element == FHIRResource { diff --git a/Intake/Resources/Localizable.xcstrings b/Intake/Resources/Localizable.xcstrings index 733365d..2f6caa3 100644 --- a/Intake/Resources/Localizable.xcstrings +++ b/Intake/Resources/Localizable.xcstrings @@ -63,9 +63,6 @@ }, "Add Field" : { - }, - "ADD_MEDICATION" : { - }, "ADD_REACTION" : { @@ -176,9 +173,6 @@ } } } - }, - "DELETE_MEDICATION" : { - }, "DELETE_REACTION" : { @@ -295,7 +289,7 @@ "Medical Intake Form" : { }, - "Medication" : { + "Medication Settings" : { }, "Medications" : { @@ -429,9 +423,6 @@ }, "Please list conditions you have had" : { - }, - "Please list your current medications." : { - }, "Primary Concern" : { From 43c9f958c8c7f36f3c8f34480da3a5aa5a1e86b8 Mon Sep 17 00:00:00 2001 From: Nina Boord <86579493+ninaboord@users.noreply.github.com> Date: Mon, 4 Mar 2024 01:45:13 +0100 Subject: [PATCH 2/3] Feature/llm new (#50) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # *LLM Patient Context Update* ## :recycle: Current situation & Problem *Before this PR, chief complaint was very impersonal and was not able to access basic information about the patient nor greet them by their name. Now it does! ## :gear: Release Notes * on appear, if HealthKit is connected, the LLM is able to now access the patient's name, age, and gender in order to provide further context about the patient when asking questions. the LLM can now personalize questions by using the patient's first name. furthermore, the LLM will use more simpler terms if the patient is younger. * the entire patient description from FHIRStore is not passed into the LLM -- only the required values. this is important because the patient description includes sensitive information such as address and social security number. * I'm excited about the potential to give the LLM more personalized context about the patient. For example, if the patient came in with coughing issues and the LLM knew that the patient had a history of asthma, it would be super cool if the chief complaint was able to ask questions about that history such as (does it feel like this cough is triggered by your asthma symptoms or unrelated to your symptoms?). I am thinking that the chief complaint should actually go at the end of the NavigationStack, and then I can pass in the newly created environment variables for each view as more context for the LLM about the patient and modify the system prompt and summarize functions accordingly. This will be my next step! Learning how to pass in this more simple context is most of the way there! Here is some photos of the personalization of the LLM: Screenshot 2024-02-29 at 12 55 13 PM Screenshot 2024-02-29 at 12 53 09 PM ## :books: Documentation *Please ensure that you properly document any additions in conformance to [Spezi Documentation Guide](https://github.com/StanfordSpezi/.github/blob/main/DOCUMENTATIONGUIDE.md).* *You can use this section to describe your solution, but we encourage contributors to document your reasoning and changes using in-line documentation.* getValue fetches the patient's full name, gender, and date of birth from FHIRStore. since "name" is stored differently in the patient description (in an array) it fetches fullName slightly differently than it fetches gender and date of birth. calculateAge fetches the patient's date of birth from FHIRStore and calculates age based on today's date. it then returns the patient's age as a string ## :white_check_mark: Testing *Please ensure that the PR meets the testing requirements set by CodeCov and that new functionality is appropriately tested.* *This section describes important information about the tests and why some elements might not be testable.* Right now, this code is not specifically tested but I used it on several patients and it worked great. it includes handling if there is no patient context from HealthKit and will still work fine in that case. ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [x] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --------- Co-authored-by: nriedman <108841122+nriedman@users.noreply.github.com> --- Intake.xcodeproj/project.pbxproj | 6 +- Intake/ChiefComplaint/LLMInteraction.swift | 166 ++++++++++++------ Intake/Resources/Localizable.xcstrings | 11 ++ ..._ad134528-56a5-35fd-c37f-466ff119c625.json | 0 ...8-56a5-35fd-c37f-466ff119c625.json.license | 0 ..._5b3645de-a2d0-d016-0839-bab3757c4c58.json | 0 ...e-a2d0-d016-0839-bab3757c4c58.json.license | 0 ..._9c3df38a-d3b7-2198-3898-51f9153d023d.json | 0 ...a-d3b7-2198-3898-51f9153d023d.json.license | 0 ..._ed70a28f-30b2-acb7-658a-8b340dadd685.json | 0 ...f-30b2-acb7-658a-8b340dadd685.json.license | 0 ..._e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json | 0 ...a-22a7-d166-7bb1-63f6bbce1a32.json.license | 0 ..._d66b5418-06cb-fc8a-8c13-85685b6ac939.json | 0 ...8-06cb-fc8a-8c13-85685b6ac939.json.license | 0 15 files changed, 131 insertions(+), 52 deletions(-) rename Intake/Resources/{Mock Patients => MockPatients}/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json.license (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json.license (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json.license (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json (100%) rename Intake/Resources/{Mock Patients => MockPatients}/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license (100%) diff --git a/Intake.xcodeproj/project.pbxproj b/Intake.xcodeproj/project.pbxproj index dd38b8e..84046bc 100644 --- a/Intake.xcodeproj/project.pbxproj +++ b/Intake.xcodeproj/project.pbxproj @@ -321,7 +321,7 @@ 2FE5DC2D29EDD792004B9AB4 /* Resources */ = { isa = PBXGroup; children = ( - 5A2B9F912B69E32A005CA63F /* Mock Patients */, + 5A2B9F912B69E32A005CA63F /* MockPatients */, 653A255428338800005D4D48 /* Assets.xcassets */, 2FA0BFEC2ACC977500E0EF83 /* Localizable.xcstrings */, 2FE5DC2C29EDD78E004B9AB4 /* ConsentDocument.md */, @@ -423,7 +423,7 @@ path = "Mock Data"; sourceTree = ""; }; - 5A2B9F912B69E32A005CA63F /* Mock Patients */ = { + 5A2B9F912B69E32A005CA63F /* MockPatients */ = { isa = PBXGroup; children = ( 5A2B9F922B69E32A005CA63F /* Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json */, @@ -439,7 +439,7 @@ 5A2B9F9C2B69E32A005CA63F /* Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license */, 5A2B9F9D2B69E32A005CA63F /* Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license */, ); - path = "Mock Patients"; + path = MockPatients; sourceTree = ""; }; 5A2B9FB32B6AFE1F005CA63F /* Allergy Records */ = { diff --git a/Intake/ChiefComplaint/LLMInteraction.swift b/Intake/ChiefComplaint/LLMInteraction.swift index 4fc2386..92bb2d0 100644 --- a/Intake/ChiefComplaint/LLMInteraction.swift +++ b/Intake/ChiefComplaint/LLMInteraction.swift @@ -11,12 +11,77 @@ // SPDX-License-Identifier: MIT // +import Foundation import SpeziChat +import SpeziFHIR import SpeziLLM import SpeziLLMLocal import SpeziLLMOpenAI import SwiftUI +func calculateAge(from dobString: String, with format: String = "yyyy-MM-dd") -> String { + if dobString.isEmpty { + return "" + } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = format + + guard let birthDate = dateFormatter.date(from: dobString) else { + return "Invalid date format or date string." + } + + let ageComponents = Calendar.current.dateComponents([.year], from: birthDate, to: Date()) + if let age = ageComponents.year { + return "\(age)" + } else { + return "Could not calculate age" + } +} + +func getValue(forKey key: String, from jsonString: String) -> String? { + guard let jsonData = jsonString.data(using: .utf8) else { + print("Error: Cannot create Data from JSON string") + return nil + } + + do { + if let dictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] { + if key == "name" { + if let nameArray = dictionary[key] as? [[String: Any]], !nameArray.isEmpty { + let nameDict = nameArray[0] // Accessing the first name object + if let family = nameDict["family"] as? String, + let givenArray = nameDict["given"] as? [String], + !givenArray.isEmpty { + let given = givenArray.joined(separator: " ") // Assuming there might be more than one given name + + return "\(given) \(family)" + } + } + } else { + return dictionary[key] as? String + } + } else { + print("Error: JSON is not a dictionary") + } + } catch { + print("Error: \(error.localizedDescription)") + } + + return nil +} + +func getInfo(patient: FHIRResource, field: String) -> String { + let jsonDescription = patient.jsonDescription + + if let infoValue = getValue(forKey: field, from: jsonDescription) { + print("Info found: \(infoValue)") + return infoValue + } + + print("Key \(field) not found") + return "" +} + struct LLMInteraction: View { @Observable @@ -36,11 +101,16 @@ struct LLMInteraction: View { static let name: String = "summarize_complaint" static let description: String = """ When there is enough information to give to the doctor,\ - summarize the conversation into a concise Chief Complaint. + summarize the conversation into a concise Chief Complaint.\ + Then call the summerize_complaint function. """ - - @Parameter(description: "A summary of the patient's primary concern.") var patientSummary: String - + + static let summaryDescription = """ + A summary of the patient's primary concern. Include a sentence introducing the patient's name,\ + age, and gender, if you have access to this information. + """ + @Parameter(description: summaryDescription) var patientSummary: String + let stringBox: StringBox init(stringBox: StringBox) { @@ -54,6 +124,8 @@ struct LLMInteraction: View { } } + @Environment(LLMRunner.self) var runner: LLMRunner + @Environment(FHIRStore.self) private var fhirStore @Environment(DataStore.self) private var data @Environment(NavigationPathWrapper.self) private var navigationPath @@ -62,8 +134,10 @@ struct LLMInteraction: View { @State var showOnboarding = true @State var greeting = true + @State var stringBox: StringBox = .init() - + @State var showSheet = false + var body: some View { @Bindable var data = data @@ -80,9 +154,42 @@ struct LLMInteraction: View { } .onAppear { + var fullName: String = "" + var firstName: String = "" + var dob: String = "" + var gender: String = "" + + if let patient = fhirStore.patient { + fullName = getInfo(patient: patient, field: "name").filter { !$0.isNumber } + dob = getInfo(patient: patient, field: "birthDate") + gender = getInfo(patient: patient, field: "gender") + + let age = calculateAge(from: dob) + let nameString = fullName.components(separatedBy: " ") + + if let firstNameValue = nameString.first { + firstName = firstNameValue + } else { + print("First Name is empty") + } + + let systemMessage = """ + The first name of the patient is \(String(describing: firstName)) and the patient is \(String(describing: age))\ + years old. The patient's gender is \(String(describing: gender)) Please speak with\ + the patient as you would a person of this age group, using as simple words as possible\ + if the patient is young. Address them by their first name when you ask questions. + """ + session.context.append( + systemMessage: systemMessage + ) + } + if greeting { - let assistantMessage = ChatEntity(role: .assistant, content: "Hello! What brings you to the doctor's office?") - session.context.insert(assistantMessage, at: 0) + if firstName.isEmpty { + session.context.append(assistantOutput: "Hello! What brings you to the doctor's office?") + } else { + session.context.append(assistantOutput: "Hello \(String(describing: firstName))! What brings you to the doctor's office?") + } } greeting = false } @@ -92,8 +199,8 @@ struct LLMInteraction: View { self.showSummary() } } - - init(presentingAccount: Binding) { // swiftlint:disable:this function_body_length + + init(presentingAccount: Binding) { self._presentingAccount = presentingAccount let temporaryStringBox = StringBox() self.stringBox = temporaryStringBox @@ -101,46 +208,7 @@ struct LLMInteraction: View { schema: LLMOpenAISchema( parameters: .init( modelType: .gpt3_5Turbo, - systemPrompt: """ - Pretend you are a nurse. Your job is to gather information about the medical concern of a patient.\ - Your job is to provide a summary of the patient’s chief medical complaint to the doctor so that the doctor\ - has all of the information they need to begin the appointment. Ask questions specific to the concern of the\ - patient in order to help clarify their chief complaint into a concise, specific concern. Ask the patient to\ - elaborate a little bit if you feel that they are not providing sufficient information. You should always ask about\ - severity and onset, and if relevant to the specific condition, you might specific questions about the location,\ - laterality, triggers, character, timing, description, progression, and associated symptoms unique to the complaint.\ - Ask with empathy.\ - Headache:\ - Onset: When did the headache start? Location: Where is the pain located? Duration: How long does each headache episode last?\ - Severity: On a scale of 1 to 10, how would you rate the pain? Triggers: Are there any specific triggers\ - that seem to bring on the headache? Associated Symptoms: Do you experience nausea, vomiting, sensitivity to light or sound?\ - Abdominal Pain:\ - Location: Where is the pain located abdomen? Character: How would you describe the pain (e.g., sharp, dull, cramping)?\ - Severity: On a scale of 1 to 10, how severe is the pain? Timing: Does the pain come and go, or is it constant?\ - Associated Symptoms: Any nausea, vomiting, or other changes in your bowl?\ - Fever:\ - Temperature: What is your current temperature?\ - Onset: When did the fever start?\ - Duration: How long have you had the fever?\ - Associated Symptoms: Any chills, sweating, body aches?\ - Recent Travel or Exposure: Have you traveled recently?\ - Have you been around anyone who was sick? - Rash:\ - Onset: When did the rash first appear? Location: Where is the rash located on your body? Description: How would you\ - describe the rash (e.g., raised, itchy, red)? Progression: Has the rash changed in appearance since it first appeared?\ - Associated Symptoms: Any fever, itching, pain?\ - Joint Pain:\ - Location: Which joints are affected? Onset: When did the joint pain start? Character: How would you describe the pain\ - (e.g., sharp, dull, achy)? Timing: Does the pain occur at specific times of the day or with certain activities?\ - Associated Symptoms: Any swelling, redness, stiffness?\ - Fatigue: Onset: When did you start feeling fatigued? Duration: How long have you been experiencing fatigue?\ - Severity: On a scale of 1 to 10, how would you rate your fatigue? Triggers: Is there anything that seems to\ - make your fatigue better or worse? Associated Symptoms: Any changes in appetite, sleep disturbances?\ - As you can see by the examples, you should ask questions specific to the patient's symptoms. If relevant, you should\ - ask follow-up questions to the patient's responses in order to gather more information if you feel it is needed. \ - Please use everyday layman terms and avoid using complex medical terminology.\ - Only ask one question or prompt at a time, and keep your questions brief (one to two short sentences). - """ + systemPrompt: "CHIEF_COMPLAINT_SYSTEM_PROMPT".localized().localizedString() ) ) { SummarizeFunction(stringBox: temporaryStringBox) diff --git a/Intake/Resources/Localizable.xcstrings b/Intake/Resources/Localizable.xcstrings index 2f6caa3..ae2d9ea 100644 --- a/Intake/Resources/Localizable.xcstrings +++ b/Intake/Resources/Localizable.xcstrings @@ -87,6 +87,17 @@ }, "Auto-fill Intake Form" : { + }, + "CHIEF_COMPLAINT_SYSTEM_PROMPT" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pretend you are a nurse. Your job is to gather information about the medical concern of a patient.\\\n Your job is to provide a summary of the patient’s chief medical complaint to the doctor so that the doctor\\\n has all of the information they need to begin the appointment. Ask questions specific to the concern of the\\\n patient in order to help clarify their chief complaint into a concise, specific concern. Ask the patient to\\\n elaborate a little bit if you feel that they are not providing sufficient information. You should always ask about\\\n severity and onset, and if relevant to the specific condition, you might specific questions about the location,\\\n laterality, triggers, character, timing, description, progression, and associated symptoms unique to the complaint. If you are able to find the answer to a question based on the patient's past responses, then do not ask the question. \\\n Ask with empathy.\\\n Headache:\\\n Onset: When did the headache start? Location: Where is the pain located? Duration: How long does each headache episode last?\\\n Severity: On a scale of 1 to 10, how would you rate the pain? Triggers: Are there any specific triggers\\\n that seem to bring on the headache? Associated Symptoms: Do you experience nausea, vomiting, sensitivity to light or sound?\\\n Abdominal Pain:\\\n Location: Where is the pain located abdomen? Character: How would you describe the pain (e.g., sharp, dull, cramping)?\\\n Severity: On a scale of 1 to 10, how severe is the pain? Timing: Does the pain come and go, or is it constant?\\\n Associated Symptoms: Any nausea, vomiting, or other changes in your bowl?\\\n Fever:\\\n Temperature: What is your current temperature?\\\n Onset: When did the fever start?\\\n Duration: How long have you had the fever?\\\n Associated Symptoms: Any chills, sweating, body aches?\\\n Recent Travel or Exposure: Have you traveled recently?\\\n Have you been around anyone who was sick?\n Rash:\\\n Onset: When did the rash first appear? Location: Where is the rash located on your body? Description: How would you\\\n describe the rash (e.g., raised, itchy, red)? Progression: Has the rash changed in appearance since it first appeared?\\\n Associated Symptoms: Any fever, itching, pain?\\\n Joint Pain:\\\n Location: Which joints are affected? Onset: When did the joint pain start? Character: How would you describe the pain\\\n (e.g., sharp, dull, achy)? Timing: Does the pain occur at specific times of the day or with certain activities?\\\n Associated Symptoms: Any swelling, redness, stiffness?\\\n Fatigue: Onset: When did you start feeling fatigued? Duration: How long have you been experiencing fatigue?\\\n Severity: On a scale of 1 to 10, how would you rate your fatigue? Triggers: Is there anything that seems to\\\n make your fatigue better or worse? Associated Symptoms: Any changes in appetite, sleep disturbances?\\\n As you can see by the examples, you should ask questions specific to the patient's symptoms. If relevant, you should\\\n ask follow-up questions to the patient's responses in order to gather more information if you feel it is needed. \\\n Please use everyday layman terms and avoid using complex medical terminology.\\\n Only ask one question or prompt at a time, and keep your questions brief (one to two short sentences).\\\n As a nurse, you should decline to answer any user inputs that are completely irrelevant to your objective of\\\n understanding your patient's chief complaint and past health history." + } + } + } }, "CLOSE" : { "comment" : "MARK: General", diff --git a/Intake/Resources/Mock Patients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json b/Intake/Resources/MockPatients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json similarity index 100% rename from Intake/Resources/Mock Patients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json rename to Intake/Resources/MockPatients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json diff --git a/Intake/Resources/Mock Patients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json.license b/Intake/Resources/MockPatients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json.license rename to Intake/Resources/MockPatients/Allen322_Ferry570_ad134528-56a5-35fd-c37f-466ff119c625.json.license diff --git a/Intake/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json b/Intake/Resources/MockPatients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json similarity index 100% rename from Intake/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json rename to Intake/Resources/MockPatients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json diff --git a/Intake/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license b/Intake/Resources/MockPatients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license rename to Intake/Resources/MockPatients/Beatris270_Bogan287_5b3645de-a2d0-d016-0839-bab3757c4c58.json.license diff --git a/Intake/Resources/Mock Patients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json b/Intake/Resources/MockPatients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json similarity index 100% rename from Intake/Resources/Mock Patients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json rename to Intake/Resources/MockPatients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json diff --git a/Intake/Resources/Mock Patients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license b/Intake/Resources/MockPatients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license rename to Intake/Resources/MockPatients/Edythe31_Morar593_9c3df38a-d3b7-2198-3898-51f9153d023d.json.license diff --git a/Intake/Resources/Mock Patients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json b/Intake/Resources/MockPatients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json similarity index 100% rename from Intake/Resources/Mock Patients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json rename to Intake/Resources/MockPatients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json diff --git a/Intake/Resources/Mock Patients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json.license b/Intake/Resources/MockPatients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json.license rename to Intake/Resources/MockPatients/Gonzalo160_Duenas839_ed70a28f-30b2-acb7-658a-8b340dadd685.json.license diff --git a/Intake/Resources/Mock Patients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json b/Intake/Resources/MockPatients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json similarity index 100% rename from Intake/Resources/Mock Patients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json rename to Intake/Resources/MockPatients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json diff --git a/Intake/Resources/Mock Patients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json.license b/Intake/Resources/MockPatients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json.license rename to Intake/Resources/MockPatients/Jacklyn830_Veum823_e0e1f21a-22a7-d166-7bb1-63f6bbce1a32.json.license diff --git a/Intake/Resources/Mock Patients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json b/Intake/Resources/MockPatients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json similarity index 100% rename from Intake/Resources/Mock Patients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json rename to Intake/Resources/MockPatients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json diff --git a/Intake/Resources/Mock Patients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license b/Intake/Resources/MockPatients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license similarity index 100% rename from Intake/Resources/Mock Patients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license rename to Intake/Resources/MockPatients/Milton509_Ortiz186_d66b5418-06cb-fc8a-8c13-85685b6ac939.json.license From d58015daa51e3a8d2e8b9e72a0bddddcabfd8c97 Mon Sep 17 00:00:00 2001 From: Zoya Garg <125168583+zoyagarg@users.noreply.github.com> Date: Mon, 4 Mar 2024 21:00:14 -0800 Subject: [PATCH 3/3] Social History: UI overhaul + lint fixes + math fixes (#52) # Social History UI Overhaul ## :recycle: Current situation & Problem * Fixed UI so it was consistent ## :white_check_mark: Testing * We're doing testing Week 9! ## :pencil: Code of Conduct & Contributing Guidelines By submitting creating this pull request, you agree to follow our [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md): - [ ] I agree to follow the [Code of Conduct](https://github.com/CS342/.github/blob/main/CODE_OF_CONDUCT.md) and [Contributing Guidelines](https://github.com/CS342/.github/blob/main/CONTRIBUTING.md). --------- Co-authored-by: Vishnu Ravi Co-authored-by: Vishnu Ravi --- Intake.xcodeproj/project.pbxproj | 12 +- .../xcshareddata/swiftpm/Package.resolved | 392 ------------------ Intake/Allergy Records/AllergyRecords.swift | 2 +- Intake/Home.swift | 11 +- Intake/Resources/Localizable.xcstrings | 64 +-- Intake/SocialHistory/MenstrualHistory.swift | 212 ++++++++++ Intake/SocialHistory/SmokingHistory.swift | 72 ++++ .../SocialHistoryQuestions.swift | 280 ------------- IntakeUITests/LaunchTests.swift | 1 + 9 files changed, 334 insertions(+), 712 deletions(-) delete mode 100644 Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 Intake/SocialHistory/MenstrualHistory.swift create mode 100644 Intake/SocialHistory/SmokingHistory.swift delete mode 100644 Intake/SocialHistory/SocialHistoryQuestions.swift diff --git a/Intake.xcodeproj/project.pbxproj b/Intake.xcodeproj/project.pbxproj index 84046bc..d3b07df 100644 --- a/Intake.xcodeproj/project.pbxproj +++ b/Intake.xcodeproj/project.pbxproj @@ -104,8 +104,9 @@ A9D83F962B083794000D0C78 /* SpeziFirebaseAccountStorage in Frameworks */ = {isa = PBXBuildFile; productRef = A9D83F952B083794000D0C78 /* SpeziFirebaseAccountStorage */; }; A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9DFE8A82ABE551400428242 /* AccountButton.swift */; }; A9FE7AD02AA39BAB0077B045 /* AccountSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */; }; - AC2A17292B70686000F560D0 /* SocialHistoryQuestions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC2A17282B70686000F560D0 /* SocialHistoryQuestions.swift */; }; ACAA47812B571C800032D21F /* Questionnaire.json in Resources */ = {isa = PBXBuildFile; fileRef = ACAA47802B571C7F0032D21F /* Questionnaire.json */; }; + ACFFA1CE2B8FD7190006E6D4 /* SmokingHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFFA1CD2B8FD7190006E6D4 /* SmokingHistory.swift */; }; + ACFFA1D12B8FD8BB0006E6D4 /* MenstrualHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ACFFA1D02B8FD8BB0006E6D4 /* MenstrualHistory.swift */; }; F42AB1D22B6379B5002E13A6 /* SpeziLLM in Frameworks */ = {isa = PBXBuildFile; productRef = F42AB1D12B6379B5002E13A6 /* SpeziLLM */; }; F42AB1D42B6379B5002E13A6 /* SpeziLLMLocal in Frameworks */ = {isa = PBXBuildFile; productRef = F42AB1D32B6379B5002E13A6 /* SpeziLLMLocal */; }; F42AB1D62B6379B5002E13A6 /* SpeziLLMLocalDownload in Frameworks */ = {isa = PBXBuildFile; productRef = F42AB1D52B6379B5002E13A6 /* SpeziLLMLocalDownload */; }; @@ -210,8 +211,9 @@ A9720E422ABB68CC00872D23 /* AccountSetupHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSetupHeader.swift; sourceTree = ""; }; A9DFE8A82ABE551400428242 /* AccountButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountButton.swift; sourceTree = ""; }; A9FE7ACF2AA39BAB0077B045 /* AccountSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSheet.swift; sourceTree = ""; }; - AC2A17282B70686000F560D0 /* SocialHistoryQuestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialHistoryQuestions.swift; sourceTree = ""; }; ACAA47802B571C7F0032D21F /* Questionnaire.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Questionnaire.json; sourceTree = ""; }; + ACFFA1CD2B8FD7190006E6D4 /* SmokingHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokingHistory.swift; sourceTree = ""; }; + ACFFA1D02B8FD8BB0006E6D4 /* MenstrualHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenstrualHistory.swift; sourceTree = ""; }; F42AB1DB2B637C8C002E13A6 /* LLMOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMOnboardingView.swift; sourceTree = ""; }; F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMInteraction.swift; sourceTree = ""; }; F42AB1E42B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMOpenAITokenOnboarding.swift; sourceTree = ""; }; @@ -541,7 +543,8 @@ AC2A17272B70684D00F560D0 /* SocialHistory */ = { isa = PBXGroup; children = ( - AC2A17282B70686000F560D0 /* SocialHistoryQuestions.swift */, + ACFFA1D02B8FD8BB0006E6D4 /* MenstrualHistory.swift */, + ACFFA1CD2B8FD7190006E6D4 /* SmokingHistory.swift */, ); path = SocialHistory; sourceTree = ""; @@ -807,7 +810,6 @@ 2FC975A82978F11A00BA99FE /* Home.swift in Sources */, 2FE5DC4E29EDD7FA004B9AB4 /* ScheduleView.swift in Sources */, 51805C1D2B818A4400D17109 /* IntakeMedicationInstance.swift in Sources */, - AC2A17292B70686000F560D0 /* SocialHistoryQuestions.swift in Sources */, A9DFE8A92ABE551400428242 /* AccountButton.swift in Sources */, 2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */, 2F1AC9DF2B4E840E00C24973 /* Intake.docc in Sources */, @@ -818,6 +820,7 @@ 5AEA5F2C2B8680F300F1577A /* AddAllergy.swift in Sources */, 2FE5DC4029EDD7EE004B9AB4 /* FeatureFlags.swift in Sources */, 2FE5DC4629EDD7F2004B9AB4 /* Bundle+Image.swift in Sources */, + ACFFA1CE2B8FD7190006E6D4 /* SmokingHistory.swift in Sources */, F42AB1DC2B637C8C002E13A6 /* LLMOnboardingView.swift in Sources */, 2FE5DC4F29EDD7FA004B9AB4 /* EventContext.swift in Sources */, 2FE5DC5029EDD7FA004B9AB4 /* EventContextView.swift in Sources */, @@ -836,6 +839,7 @@ 5A2B9FBC2B6C7B29005CA63F /* ReactionView.swift in Sources */, 566155292AB8447C00209B80 /* Package+LicenseType.swift in Sources */, 5680DD392AB8983D004E6D4A /* PackageCell.swift in Sources */, + ACFFA1D12B8FD8BB0006E6D4 /* MenstrualHistory.swift in Sources */, F42AB1EC2B6DBF21002E13A6 /* SummaryView.swift in Sources */, 2F5E32BD297E05EA003432F8 /* IntakeDelegate.swift in Sources */, 511827962B740192002033A0 /* SurgeryView.swift in Sources */, diff --git a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index 67a1a63..0000000 --- a/Intake.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,392 +0,0 @@ -{ - "pins" : [ - { - "identity" : "abseil-cpp-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/abseil-cpp-binary.git", - "state" : { - "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", - "version" : "1.2022062300.0" - } - }, - { - "identity" : "app-check", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/app-check.git", - "state" : { - "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", - "version" : "10.18.1" - } - }, - { - "identity" : "fhirmodels", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/FHIRModels", - "state" : { - "revision" : "861afd5816a98d38f86220eab2f812d76cad84a0", - "version" : "0.5.0" - } - }, - { - "identity" : "firebase-ios-sdk", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/firebase-ios-sdk.git", - "state" : { - "revision" : "b880ec8ec927a838c51c12862c6222c30d7097d7", - "version" : "10.20.0" - } - }, - { - "identity" : "googleappmeasurement", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleAppMeasurement.git", - "state" : { - "revision" : "ceec9f28dea12b7cf3dabf18b5ed7621c88fd4aa", - "version" : "10.20.0" - } - }, - { - "identity" : "googledatatransport", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleDataTransport.git", - "state" : { - "revision" : "a732a4b47f59e4f725a2ea10f0c77e93a7131117", - "version" : "9.3.0" - } - }, - { - "identity" : "googleutilities", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/GoogleUtilities.git", - "state" : { - "revision" : "bc27fad73504f3d4af235de451f02ee22586ebd3", - "version" : "7.12.1" - } - }, - { - "identity" : "grpc-binary", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/grpc-binary.git", - "state" : { - "revision" : "a673bc2937fbe886dd1f99c401b01b6d977a9c98", - "version" : "1.49.1" - } - }, - { - "identity" : "gtm-session-fetcher", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/gtm-session-fetcher.git", - "state" : { - "revision" : "76135c9f4e1ac85459d5fec61b6f76ac47ab3a4c", - "version" : "3.3.1" - } - }, - { - "identity" : "healthkitonfhir", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/HealthKitOnFHIR.git", - "state" : { - "revision" : "825e96007d83ed83f81ee49eb3ebab29d7b7ba2f", - "version" : "0.2.5" - } - }, - { - "identity" : "interop-ios-for-google-sdks", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/interop-ios-for-google-sdks.git", - "state" : { - "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", - "version" : "100.0.0" - } - }, - { - "identity" : "leveldb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/leveldb.git", - "state" : { - "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", - "version" : "1.22.3" - } - }, - { - "identity" : "llama.cpp", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/llama.cpp", - "state" : { - "revision" : "b0611c7d3cb049822f9911878514e4706b80e2ac", - "version" : "0.1.8" - } - }, - { - "identity" : "nanopb", - "kind" : "remoteSourceControl", - "location" : "https://github.com/firebase/nanopb.git", - "state" : { - "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", - "version" : "2.30909.0" - } - }, - { - "identity" : "openai", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MacPaw/OpenAI", - "state" : { - "revision" : "35afc9a6ee127b8f22a85a31aec2036a987478af", - "version" : "0.2.6" - } - }, - { - "identity" : "promises", - "kind" : "remoteSourceControl", - "location" : "https://github.com/google/promises.git", - "state" : { - "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", - "version" : "2.3.1" - } - }, - { - "identity" : "researchkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/ResearchKit", - "state" : { - "revision" : "209164ed20592a2213c4bd69cefcb078d9de0692", - "version" : "2.2.21" - } - }, - { - "identity" : "researchkitonfhir", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/ResearchKitOnFHIR", - "state" : { - "revision" : "ea4d9691591594177e7dfbc8c246324855d73eb5", - "version" : "1.0.1" - } - }, - { - "identity" : "semaphore", - "kind" : "remoteSourceControl", - "location" : "https://github.com/groue/Semaphore.git", - "state" : { - "revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", - "version" : "0.0.8" - } - }, - { - "identity" : "spezi", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/Spezi", - "state" : { - "revision" : "0ced3efbc2af9513c07ac913ad762c773a00a6c8", - "version" : "1.2.1" - } - }, - { - "identity" : "speziaccount", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziAccount.git", - "state" : { - "revision" : "714f01ae1e67bf9c1c0e7c07624380f9bea772b7", - "version" : "1.1.0" - } - }, - { - "identity" : "spezichat", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziChat.git", - "state" : { - "revision" : "eae5c15b211f18e09aa98de63ce119629320afeb", - "version" : "0.1.8" - } - }, - { - "identity" : "spezicontact", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziContact.git", - "state" : { - "revision" : "494b776f8c98d771e4a609a1fb706097dba4c030", - "version" : "1.0.0" - } - }, - { - "identity" : "spezifhir", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziFHIR.git", - "state" : { - "revision" : "9a171a21d7028042c30a24c661bfdfc6e363c9c3", - "version" : "0.6.0" - } - }, - { - "identity" : "spezifirebase", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziFirebase.git", - "state" : { - "revision" : "ca1edf678ec59e76c9869ee3448e6e165d9c2789", - "version" : "1.0.0" - } - }, - { - "identity" : "spezifoundation", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziFoundation.git", - "state" : { - "revision" : "662f25d6010a94faf4fd996e184617fcb2bf13d4", - "version" : "1.0.3" - } - }, - { - "identity" : "spezihealthkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziHealthKit.git", - "state" : { - "revision" : "b40695ffa4d1c9d58c5a0ee277640c2343fb5516", - "version" : "0.5.1" - } - }, - { - "identity" : "spezillm", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziLLM", - "state" : { - "revision" : "6892c5dfe258371b6f3287f02b8fec57a611ba70", - "version" : "0.7.0" - } - }, - { - "identity" : "spezimedication", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziMedication.git", - "state" : { - "revision" : "95ca9aebbd23f3842639d6e322785a0ff3620aac", - "version" : "0.4.0" - } - }, - { - "identity" : "spezimockwebservice", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziMockWebService.git", - "state" : { - "revision" : "b18067d3499e630bbd995ef05a296ef8fdd42528", - "version" : "1.0.0" - } - }, - { - "identity" : "spezionboarding", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziOnboarding", - "state" : { - "revision" : "91463ae190611bd14ef52b0657e8db3bf53c9ae8", - "version" : "1.1.0" - } - }, - { - "identity" : "speziquestionnaire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git", - "state" : { - "revision" : "fac0bb02f7027b4c09bd7afdad55eb7b47ec67f3", - "version" : "1.0.1" - } - }, - { - "identity" : "spezischeduler", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziScheduler.git", - "state" : { - "revision" : "adf793cb47dc199f8ae88f5c719f4d3ba06a4c4e", - "version" : "0.8.0" - } - }, - { - "identity" : "spezispeech", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziSpeech", - "state" : { - "revision" : "a1e1d021d8f605b5e6b23aee773115d7125a57e3", - "version" : "1.0.0" - } - }, - { - "identity" : "spezistorage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziStorage.git", - "state" : { - "revision" : "eaed2220375c35400aa69d1f96a8d32b7e66b1c7", - "version" : "1.0.0" - } - }, - { - "identity" : "speziviews", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordSpezi/SpeziViews.git", - "state" : { - "revision" : "d49f716e4a4d634604bb0dcd6d53df679b6c1358", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-argument-parser", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", - "state" : { - "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", - "version" : "1.3.0" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections.git", - "state" : { - "revision" : "d029d9d39c87bed85b1c50adee7c41795261a192", - "version" : "1.0.6" - } - }, - { - "identity" : "swift-package-list", - "kind" : "remoteSourceControl", - "location" : "https://github.com/FelixHerrmann/swift-package-list", - "state" : { - "revision" : "412180a72b9a1f8262213c16459e3533b0385ea5", - "version" : "3.1.0" - } - }, - { - "identity" : "swift-protobuf", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-protobuf.git", - "state" : { - "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", - "version" : "1.25.2" - } - }, - { - "identity" : "xctestextensions", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/XCTestExtensions.git", - "state" : { - "revision" : "fb7fcee97c574b950e03b0a53874e26db27db2fe", - "version" : "0.4.8" - } - }, - { - "identity" : "xcthealthkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/XCTHealthKit.git", - "state" : { - "revision" : "6e9344a2d632b801d94fe3bbd1d891817e032103", - "version" : "0.3.5" - } - }, - { - "identity" : "xctruntimeassertions", - "kind" : "remoteSourceControl", - "location" : "https://github.com/StanfordBDHG/XCTRuntimeAssertions", - "state" : { - "revision" : "51da3403f128b120705571ce61e0fe190f8889e6", - "version" : "1.0.1" - } - } - ], - "version" : 2 -} diff --git a/Intake/Allergy Records/AllergyRecords.swift b/Intake/Allergy Records/AllergyRecords.swift index 163a287..9176c92 100644 --- a/Intake/Allergy Records/AllergyRecords.swift +++ b/Intake/Allergy Records/AllergyRecords.swift @@ -128,7 +128,7 @@ struct AllergyList: View { } private func submitAction() { - navigationPath.path.append(NavigationViews.social) + navigationPath.path.append(NavigationViews.menstrual) } private func addAllergyAction() { diff --git a/Intake/Home.swift b/Intake/Home.swift index 0d2fe2e..85c63ec 100644 --- a/Intake/Home.swift +++ b/Intake/Home.swift @@ -14,7 +14,8 @@ enum NavigationViews: String { case allergies case surgical case medical - case social + case menstrual + case smoking case medication case chat case concern @@ -88,13 +89,15 @@ struct HomeView: View { .navigationDestination(for: NavigationViews.self) { view in switch view { - case .chat: LLMInteraction(presentingAccount: $presentingAccount) - case .allergies: AllergyList() + case .smoking: SmokingHistoryView() +// case .chat: LLMAssistantView(presentingAccount: $presentingAccount) +// case .allergies: AllergyList() case .surgical: SurgeryView() case .medical: MedicalHistoryView() - case .social: SocialHistoryQuestionView() case .medication: MedicationContentView() + case .menstrual: SocialHistoryQuestionView() case .concern: SummaryView(chiefComplaint: $data.chiefComplaint) + default: SocialHistoryQuestionView() } } } diff --git a/Intake/Resources/Localizable.xcstrings b/Intake/Resources/Localizable.xcstrings index ae2d9ea..24d23b2 100644 --- a/Intake/Resources/Localizable.xcstrings +++ b/Intake/Resources/Localizable.xcstrings @@ -69,9 +69,15 @@ }, "ADD_SURGERY" : { + }, + "Additional Details" : { + }, "Additional details: %@" : { + }, + "Additional Symptoms" : { + }, "AI-assisted medical intake" : { @@ -191,14 +197,20 @@ "DETAILS" : { }, - "Do you currently smoke or have you in the past?" : { + "Download your medical records from your health system." : { }, - "Download your medical records from your health system." : { + "End Date: %@" : { }, "END_CALENDAR" : { + }, + "Ex: Heavy bleeding on second day, fatigue..." : { + + }, + "Ex: Smoked for 10 years, quit 5 years ago..." : { + }, "FHIR_RESOURCES_CHAT_CANCEL" : { @@ -305,6 +317,9 @@ }, "Medications" : { + }, + "Menstrual Information" : { + }, "Message" : { @@ -367,9 +382,6 @@ }, "Next" : { - }, - "Next: Smoking History" : { - }, "NOTIFICATION_PERMISSIONS_BUTTON" : { "extractionState" : "stale", @@ -426,10 +438,10 @@ } } }, - "Optional symptoms: heavy bleeding, cramps, etc." : { + "Other important questions." : { }, - "Other important questions." : { + "Pack years: %.2f" : { }, "Please list conditions you have had" : { @@ -521,16 +533,16 @@ } } }, - "Select approx. end date." : { + "Select End Date" : { }, - "Select approx. start date." : { + "Select Start Date" : { }, - "Select End Date" : { + "Select your last period's end date" : { }, - "Select Start Date" : { + "Select your last period's start date" : { }, "SETTINGS" : { @@ -544,12 +556,18 @@ }, "Skip" : { + }, + "Smoking History" : { + }, "Social History" : { }, "Start" : { + }, + "Start Date: %@" : { + }, "START_CALENDAR" : { @@ -565,9 +583,6 @@ }, "Summarize your surgical history." : { - }, - "Summary" : { - }, "Surgery" : { @@ -577,6 +592,9 @@ }, "Surgical History" : { + }, + "Symptoms: %@" : { + }, "TASK_CONTEXT_ACTION_QUESTIONNAIRE" : { "localizations" : { @@ -629,18 +647,12 @@ } } } - }, - "Thank you for completing Social History!" : { - }, "This application will help autocomplete your medical intake form." : { }, "Together we will summarize..." : { - }, - "Total calculated result for smoking: %.2f pack years" : { - }, "Use HealthKit Resources" : { @@ -657,18 +669,8 @@ "What is your surgical history?" : { }, - "You can include decimal values (e.g., 0.25) for the number of packs per day." : { + "Your Responses" : { - }, - "Your last period was from %@ to %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Your last period was from %1$@ to %2$@" - } - } - } } }, "version" : "1.0" diff --git a/Intake/SocialHistory/MenstrualHistory.swift b/Intake/SocialHistory/MenstrualHistory.swift new file mode 100644 index 0000000..82ee93a --- /dev/null +++ b/Intake/SocialHistory/MenstrualHistory.swift @@ -0,0 +1,212 @@ +// +// MenstrualHistory.swift +// Intake +// +// Created by Zoya Garg on 2/28/24. +// +// This source file is part of the Intake based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT + +import Foundation +import HealthKit +import SwiftUI + + +struct SectionHeader: View { + let title: String + let subtitle: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.title) + .multilineTextAlignment(.leading) + .padding(.bottom, 2) + + if !subtitle.isEmpty { + Text(subtitle) + .font(.subheadline) + .foregroundColor(.gray) + .multilineTextAlignment(.leading) + .padding(.bottom) + } + } + } +} + +struct SocialHistoryQuestionView: View { + struct SectionHeader: View { + let title: String + + var body: some View { + VStack(alignment: .leading) { + Text(title) + .font(.headline) + } + } + } + + private var menstrualCycleButtons: some View { + VStack { + Button(action: { + isSelectingStartDate = true + }) { + HStack { + Text("Select your last period's start date") + Spacer() // Add Spacer here for white space + Image(systemName: "calendar") + .accessibilityLabel(Text("START_CALENDAR")) + } + } + .buttonStyle(BorderlessButtonStyle()) + + Spacer().frame(height: 16) + + Button(action: { + isSelectingEndDate = true + }) { + HStack { + Text("Select your last period's end date") + Spacer() + Image(systemName: "calendar") + .accessibilityLabel(Text("END_CALENDAR")) + } + } + .buttonStyle(BorderlessButtonStyle()) + } + } + + private var menstrualCycleInformationSection: some View { + Group { + Section(header: Text("Menstrual Information").foregroundColor(.gray)) { + menstrualCycleButtons + } + + Section(header: Text("Additional Symptoms")) { + TextField("Ex: Heavy bleeding on second day, fatigue...", text: $additionalDetails) + } + if shouldDisplayResponses { + Section(header: Text("Your Responses").foregroundColor(.gray)) { + VStack(alignment: .leading) { + Text("Start Date: \(formatDate(startDate))") + Text("End Date: \(formatDate(endDate))") + if !additionalDetails.isEmpty { + Text("Symptoms: \(additionalDetails)") + } + } + } + } + } + } + + @State private var dateString: String = "" + @State private var additionalDetails: String = "" + @State private var isFemale = false + @State private var showMaleSlide = false + @State private var healthStore = HKHealthStore() + + @State private var isSelectingStartDate = false + @State private var isSelectingEndDate = false + @State private var startDate = Date() + @State private var endDate = Date() + @State private var lastPeriodDate: Date? + + @Environment(NavigationPathWrapper.self) private var navigationPath + + private var shouldDisplayResponses: Bool { + !additionalDetails.isEmpty || startDate != Date() || endDate != Date() + } + + + let numberFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.minimum = 0 + return formatter + }() + + var body: some View { + NavigationView { // swiftlint:disable:this closure_body_length + VStack { // swiftlint:disable:this closure_body_length + Form { + menstrualCycleInformationSection + } + .navigationTitle("Social History") + .onAppear { + fetchHealthKitData() + } + .sheet(isPresented: $isSelectingStartDate, content: { + VStack { + DatePicker("Select Start Date", selection: $startDate, displayedComponents: .date) + .datePickerStyle(GraphicalDatePickerStyle()) + + Button("Save") { + lastPeriodDate = startDate + isSelectingStartDate = false + } + } + }) + .sheet(isPresented: $isSelectingEndDate, content: { + VStack { + DatePicker("Select End Date", selection: $endDate, displayedComponents: .date) + .datePickerStyle(GraphicalDatePickerStyle()) + + Button("Save") { + isSelectingEndDate = false + } + } + }) + Spacer() + Button(action: { + navigationPath.path.append(NavigationViews.smoking) + }) { + Text("Submit") + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + } + .padding() + } + } + } + + private func formatDate(_ date: Date) -> String { + let formatter = DateFormatter() + formatter.dateStyle = .medium + return formatter.string(from: date) + } + + private func fetchHealthKitData() { + let infoToRead = Set([HKObjectType.characteristicType(forIdentifier: .biologicalSex)].compactMap { $0 }) + + Task { + do { + try await healthStore.requestAuthorization(toShare: [], read: infoToRead) + + if let bioSex = try? healthStore.biologicalSex() { + DispatchQueue.main.async { + self.isFemale = getIsFemaleBiologicalSex(biologicalSex: bioSex.biologicalSex) + self.showMaleSlide = !self.isFemale + } + } + } catch { + print("HealthKit authorization failed: \(error.localizedDescription)") + } + } + } + + private func getIsFemaleBiologicalSex(biologicalSex: HKBiologicalSex) -> Bool { + switch biologicalSex { + case .female: return true + case .male: return false + case .other: return true + case .notSet: return false + @unknown default: return false + } + } +} diff --git a/Intake/SocialHistory/SmokingHistory.swift b/Intake/SocialHistory/SmokingHistory.swift new file mode 100644 index 0000000..7ba14cb --- /dev/null +++ b/Intake/SocialHistory/SmokingHistory.swift @@ -0,0 +1,72 @@ +// +// SmokingHistory.swift +// Intake +// +// Created by Zoya Garg on 2/28/24. +// +// This source file is part of the Intake based on the Stanford Spezi Template Application project +// +// SPDX-FileCopyrightText: 2023 Stanford University +// +// SPDX-License-Identifier: MIT + +import SwiftUI + + +struct SmokingHistoryView: View { + @State private var daysPerYear: String = "" + @State private var packsPerDay: String = "" + @State private var packYears: Double = 0 + @State private var additionalDetails: String = "" + + var body: some View { + NavigationView { + VStack { + Form { + Section(header: Text("Smoking History").foregroundColor(.gray)) { + TextField("How many days a year do you smoke?", text: $daysPerYear) + .keyboardType(.decimalPad) + .onChange(of: daysPerYear) { calculatePackYears() } + .padding(.bottom, 8) + + TextField("How many packs do you smoke a day?", text: $packsPerDay) + .keyboardType(.decimalPad) + .onChange(of: packsPerDay) { calculatePackYears() } + .padding(.bottom, 8) + } + + Section(header: Text("Additional Details").foregroundColor(.gray)) { + TextField("Ex: Smoked for 10 years, quit 5 years ago...", text: $additionalDetails) + } + + // This section will automatically update when values are entered + Section(header: Text("Your Responses").foregroundColor(.gray)) { + Text("Pack years: \(packYears, specifier: "%.2f")") + if !additionalDetails.isEmpty { + Text("Additional details: \(additionalDetails)") + } + } + } + .navigationTitle("Social History") + + // The Submit button can remain for explicit submission, if required + Button("Submit") { + calculatePackYears() + } + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + .padding(.horizontal) + .padding(.bottom) + } + } + } + + func calculatePackYears() { + let days = Double(daysPerYear) ?? 0 + let packs = Double(packsPerDay) ?? 0 + packYears = (days * packs) / 365 + } +} diff --git a/Intake/SocialHistory/SocialHistoryQuestions.swift b/Intake/SocialHistory/SocialHistoryQuestions.swift deleted file mode 100644 index de80e5c..0000000 --- a/Intake/SocialHistory/SocialHistoryQuestions.swift +++ /dev/null @@ -1,280 +0,0 @@ -// -// SocialHistoryQuestions.swift -// Intake -// -// Created by Zoya Garg on 2/4/24. -// -// This source file is part of the Intake based on the Stanford Spezi Template Application project -// -// SPDX-FileCopyrightText: 2023 Stanford University -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import HealthKit -import SwiftUI - -struct SmokingSummaryView: View { - let startDate: Date - let endDate: Date - let additionalDetails: String - let totalPacksPerYear: Double - - var body: some View { - VStack { - Text("Thank you for completing Social History!") - .font(.title) - .multilineTextAlignment(.center) - .padding(.bottom) - - Text("Your last period was from \(formatDate(startDate)) to \(formatDate(endDate))") - .multilineTextAlignment(.center) - .padding(.bottom) - - if !additionalDetails.isEmpty { - Text("Additional details: \(additionalDetails)") - .multilineTextAlignment(.center) - .padding(.bottom) - } - - Text("Total calculated result for smoking: \(totalPacksPerYear, specifier: "%.2f") pack years") - .multilineTextAlignment(.center) - .padding(.bottom) - - Spacer() - } - .padding() - .navigationBarTitle("Summary", displayMode: .inline) // Set the navigation bar title for the summary view - } - - private func formatDate(_ date: Date) -> String { - let formatter = DateFormatter() - formatter.dateStyle = .long - return formatter.string(from: date) - } -} - -struct YesNoButtonView: View { - @State private var daysPerYear: Double? - @State private var packsPerDay: Double? - @State private var totalPacksPerYear: Double = 0 - @State private var navigateToSummary = false - - var startDate: Date - var endDate: Date - var additionalDetails: String - - let numberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.minimum = 0 - return formatter - }() - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - Text("Do you currently smoke or have you in the past?") - .font(.title) - .foregroundColor(.blue) - - // Question 1 - TextField("How many days a year do you smoke?", value: $daysPerYear, formatter: numberFormatter) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding(.bottom, 8) - - Text("You can include decimal values (e.g., 0.25) for the number of packs per day.") - .font(.caption) - .foregroundColor(.gray) - - // Question 2 - TextField("How many packs do you smoke a day?", value: $packsPerDay, formatter: numberFormatter) - .keyboardType(.decimalPad) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .padding(.bottom, 8) - - Button("Submit") { - calculateTotalPacksPerYear() - navigateToSummary = true - } - .padding() - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(10) - - // NOTE: Ask Zoya about deprecated NavigationLink -- replace with sheet? Integrate into main navigation stack? - NavigationLink( - destination: SmokingSummaryView( - startDate: startDate, - endDate: endDate, - additionalDetails: additionalDetails, - totalPacksPerYear: totalPacksPerYear - ), - isActive: $navigateToSummary - ) { - EmptyView() - } - } - .padding() - } - - func calculateTotalPacksPerYear() { - let days = daysPerYear ?? 0 - let packs = packsPerDay ?? 0 - totalPacksPerYear = days * packs - } -} - -struct SocialHistoryQuestionView: View { - struct SectionHeader: View { - let title: String - let subtitle: String - - var body: some View { - VStack(alignment: .leading) { - Text(title) - .font(.headline) - Text(subtitle) - .font(.subheadline) - .foregroundColor(.gray) - } - } - } - - private var menstrualCycleButtons: some View { - VStack { - Button(action: { - isSelectingStartDate = true - }) { - HStack { - Text("Select approx. start date.") - Spacer() // Add Spacer here for white space - Image(systemName: "calendar") - .accessibilityLabel(Text("START_CALENDAR")) - } - } - .buttonStyle(BorderlessButtonStyle()) - - Spacer().frame(height: 16) // White space between buttons - - Button(action: { - isSelectingEndDate = true - }) { - HStack { - Text("Select approx. end date.") - Spacer() - Image(systemName: "calendar") - .accessibilityLabel(Text("END_CALENDAR")) - } - } - .buttonStyle(BorderlessButtonStyle()) - } - } - - private var menstrualCycleInformationSection: some View { - Section(header: SectionHeader(title: "Menstrual Cycle Information", subtitle: "Select the dates of your last period.")) { - menstrualCycleButtons - TextField("Optional symptoms: heavy bleeding, cramps, etc.", text: $additionalDetails) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - } - - @State private var dateString: String = "" - @State private var additionalDetails: String = "" - @State private var isFemale = false - @State private var showMaleSlide = false - @State private var healthStore = HKHealthStore() - - @State private var isSelectingStartDate = false - @State private var isSelectingEndDate = false - @State private var startDate = Date() - @State private var endDate = Date() - @State private var lastPeriodDate: Date? - - @State private var daysPerYear: Double? - @State private var packsPerDay: Double? - @State private var totalPacksPerYear: Double = 0 - @State private var navigateToSummary = false - - // Passed parameters should not be within a function or closure - - let numberFormatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.minimum = 0 - return formatter - }() - - var body: some View { - NavigationView { - Form { - menstrualCycleInformationSection - Section { - NavigationLink(destination: YesNoButtonView(startDate: startDate, endDate: endDate, additionalDetails: additionalDetails)) { - Text("Next: Smoking History").foregroundColor(.blue) - } - } - } - .navigationTitle("Social History") - .onAppear { - fetchHealthKitData() - } - .sheet(isPresented: $isSelectingStartDate, content: { - VStack { - DatePicker("Select Start Date", selection: $startDate, displayedComponents: .date) - .datePickerStyle(GraphicalDatePickerStyle()) - - Button("Save") { - lastPeriodDate = startDate - isSelectingStartDate = false - } - } - }) - .sheet(isPresented: $isSelectingEndDate, content: { - VStack { - DatePicker("Select End Date", selection: $endDate, displayedComponents: .date) - .datePickerStyle(GraphicalDatePickerStyle()) - - Button("Save") { - // Handle saving the end date here - isSelectingEndDate = false - } - } - }) - } - } - - private func fetchHealthKitData() { - let infoToRead = Set([HKObjectType.characteristicType(forIdentifier: .biologicalSex)].compactMap { $0 }) - - Task { - do { - try await healthStore.requestAuthorization(toShare: [], read: infoToRead) - - if let bioSex = try? healthStore.biologicalSex() { - DispatchQueue.main.async { - self.isFemale = getIsFemaleBiologicalSex(biologicalSex: bioSex.biologicalSex) - self.showMaleSlide = !self.isFemale - } - } - - // Fetch and auto-populate the last menstrual period date from HealthKit - // Update the startDate and endDate variables accordingly - - } catch { - print("HealthKit authorization failed: \(error.localizedDescription)") - } - } - } - - private func getIsFemaleBiologicalSex(biologicalSex: HKBiologicalSex) -> Bool { - switch biologicalSex { - case .female: return true - case .male: return false - case .other: return true - case .notSet: return false - @unknown default: return false - } - } -} diff --git a/IntakeUITests/LaunchTests.swift b/IntakeUITests/LaunchTests.swift index 1bdee67..9c3a73c 100644 --- a/IntakeUITests/LaunchTests.swift +++ b/IntakeUITests/LaunchTests.swift @@ -24,5 +24,6 @@ class LaunchTests: XCTestCase { func testApplicationLaunch() throws { let app = XCUIApplication() XCTAssertEqual(app.state, .runningForeground) + app.buttons["Start"].tap() } }