diff --git a/Intake.xcodeproj/project.pbxproj b/Intake.xcodeproj/project.pbxproj index 6f0590b..6b7363c 100644 --- a/Intake.xcodeproj/project.pbxproj +++ b/Intake.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ F42AB1DF2B637C9D002E13A6 /* LLMInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */; }; F42AB1E52B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1E42B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift */; }; F42AB1EC2B6DBF21002E13A6 /* SummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */; }; + F42AB1F22B71B4D2002E13A6 /* AllergyViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -154,6 +155,7 @@ F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMInteraction.swift; sourceTree = ""; }; F42AB1E42B6383F9002E13A6 /* LLMOpenAITokenOnboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMOpenAITokenOnboarding.swift; sourceTree = ""; }; F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SummaryView.swift; sourceTree = ""; }; + F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllergyViewTest.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -392,6 +394,7 @@ F42AB1DB2B637C8C002E13A6 /* LLMOnboardingView.swift */, F42AB1DE2B637C9C002E13A6 /* LLMInteraction.swift */, F42AB1EB2B6DBF20002E13A6 /* SummaryView.swift */, + F42AB1F12B71B4D0002E13A6 /* AllergyViewTest.swift */, ); path = ChiefComplaint; sourceTree = ""; @@ -619,6 +622,7 @@ 2F4FC8D729EE69D300BFFE26 /* MockUpload.swift in Sources */, 2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */, F42AB1DF2B637C9D002E13A6 /* LLMInteraction.swift in Sources */, + F42AB1F22B71B4D2002E13A6 /* AllergyViewTest.swift in Sources */, 2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */, 2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */, 2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */, diff --git a/Intake/ChiefComplaint/AllergyViewTest.swift b/Intake/ChiefComplaint/AllergyViewTest.swift new file mode 100644 index 0000000..385c72a --- /dev/null +++ b/Intake/ChiefComplaint/AllergyViewTest.swift @@ -0,0 +1,25 @@ +// +// AllergyView.swift +// Intake +// +// Created by Nick Riedman on 2/5/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 SwiftUI + +struct AllergyViewTest: View { + var body: some View { + VStack { + Text("Success!") + .padding() + .multilineTextAlignment(.center) + } + } +} diff --git a/Intake/ChiefComplaint/LLMInteraction.swift b/Intake/ChiefComplaint/LLMInteraction.swift index 825e986..28d79c5 100644 --- a/Intake/ChiefComplaint/LLMInteraction.swift +++ b/Intake/ChiefComplaint/LLMInteraction.swift @@ -18,106 +18,139 @@ import SpeziLLMOpenAI import SwiftUI struct LLMInteraction: View { + @Observable + class StringBox: Equatable { + var llmResponseSummary: String + + init() { + self.llmResponseSummary = "" + } + + static func == (lhs: LLMInteraction.StringBox, rhs: LLMInteraction.StringBox) -> Bool { + lhs.llmResponseSummary == rhs.llmResponseSummary + } + } + + struct SummarizeFunction: LLMFunction { 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. """ - @Parameter(description: "The primary medical concern that the patient is experiencing.") var medicalConcern: String - - @Parameter(description: "The severity of the primary medical concern.") var severity: String + - @Parameter(description: "The duration of the primary medical concern.") var duration: String + @Parameter(description: "A summary of the patient's primary concern.") var patientSummary: String - static let desc: String = """ - Extra important information relevant to the primary\ - medical concern that the doctor should be aware of. - """ - @Parameter(description: desc) var supplementaryInfo: String + let stringBox: StringBox - @Binding var chiefComplaint: String + init(stringBox: StringBox) { + self.stringBox = stringBox + } func execute() async throws -> String? { - let summary = """ - Here is the summary that will be provided to your doctor:\n - Primary concern: \(medicalConcern)\n - Severity: \(severity)\n - Duration: \(duration)\n - Extra Info: \(supplementaryInfo)\n - """ - chiefComplaint = summary - return summary + let summary = patientSummary + self.stringBox.llmResponseSummary = summary + return nil } } - @State private var chiefComplaint: String? = "blah blah blah" - @State private var shouldNavigateToSummaryView = true @Binding var presentingAccount: Bool @Environment(LLMRunner.self) var runner: LLMRunner @State var showOnboarding = true + @State var greeting = true + @State var stringBox: StringBox + @State var showSheet = false - @State var model: LLM = LLMOpenAI( - parameters: .init( - modelType: .gpt3_5Turbo, - systemPrompt: """ - You are acting as an intake person at a clinic and need to work with\ - the patient to help clarify their chief complaint into a concise,\ - specific complaint. - - You should always ask about severity and duration if the patient does not include this information. - - Additionally, help guide the patient into providing information specific to the condition that the define.\ - For example, if the patient is experiencing leg pain, you should prompt them to be more\ - specific about laterality and location. You should also ask if the pain is dull or sharp,\ - and encourage them to rate their pain on a scale of 1 to 10. For a cough, for example, you\ - should inquire whether the cough is wet or dry, as well as any other characteristics of the\ - cough that might allow a doctor to rule out diagnoses. - - Please use everyday layman terms and avoid using complex medical terminology.\ - Only ask one question or prompt at a time, and keep your responses brief (one to two short sentences). - """ - ) - ) { -// SummarizeFunction() - } + @State var model: LLM var body: some View { - NavigationStack { - LLMChatView( - model: model - ) - .navigationTitle("Chief Complaint") - .toolbar { - if AccountButton.shouldDisplay { - AccountButton(isPresented: $presentingAccount) - } - } - .sheet(isPresented: $showOnboarding) { - LLMOnboardingView(showOnboarding: $showOnboarding) + LLMChatView( + model: model + ) + .navigationTitle("Chief Complaint") + .toolbar { + if AccountButton.shouldDisplay { + AccountButton(isPresented: $presentingAccount) } - .onAppear { + } + .sheet(isPresented: $showOnboarding) { + LLMOnboardingView(showOnboarding: $showOnboarding) + } + .onAppear { + if greeting { let assistantMessage = ChatEntity(role: .assistant, content: "Hello! What brings you to the doctor's office?") model.context.insert(assistantMessage, at: 0) } - .onChange(of: chiefComplaint) { _, newChiefComplaint in - if let newChiefComplaint = newChiefComplaint { - shouldNavigateToSummaryView = true - } - } - NavigationLink( - destination: SummaryView(chiefComplaint: chiefComplaint ?? "No Chief Complaint"), - label: { - EmptyView() - } - ) + greeting = false + } + .onChange(of: self.stringBox.llmResponseSummary) { _, _ in + self.showSheet = true } + .sheet(isPresented: $showSheet) { + SummaryView(chiefComplaint: self.stringBox.llmResponseSummary) + } + } + + init(presentingAccount: Binding) { + // swiftlint:disable closure_end_indentation + self._presentingAccount = presentingAccount + let stringBoxTemp = StringBox() + self.stringBox = stringBoxTemp + self.model = LLMOpenAI( + 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). + """ + ) + ) { + SummarizeFunction(stringBox: stringBoxTemp) + } + // swiftlint:enable closure_end_indentation } } #Preview { - LLMInteraction(presentingAccount: .constant(true)) + LLMInteraction(presentingAccount: .constant(false)) .previewWith { LLMRunner { LLMOpenAIRunnerSetupTask() diff --git a/Intake/ChiefComplaint/SummaryView.swift b/Intake/ChiefComplaint/SummaryView.swift index 31f8366..6ef692f 100644 --- a/Intake/ChiefComplaint/SummaryView.swift +++ b/Intake/ChiefComplaint/SummaryView.swift @@ -15,14 +15,44 @@ import Foundation import SwiftUI struct SummaryView: View { - let chiefComplaint: String + @State var chiefComplaint: String +// var navigationPath: NavigationPath var body: some View { - VStack { - Text(chiefComplaint) - .padding() - .multilineTextAlignment(.center) + VStack(alignment: .leading, spacing: 20) { + Text("Primary Concern") + .font(.title) + .padding(.top, 50) + + Text("Here is a summary of your primary concern:") + + TextEditor(text: $chiefComplaint) + .frame(height: 150) + .background(Color.white) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(Color.secondary, lineWidth: 1) + ) + .padding(.horizontal) + + Button(action: { + // Save output to Fhirstore and navigate to next screen +// navigationPath.append(NavigationViews.allergies) + }) { + Text("Submit") + .foregroundColor(.white) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .cornerRadius(8) + } + .padding(.horizontal) } - .navigationTitle("Summary") + .padding() + } + + init(chiefComplaint: String) { + self.chiefComplaint = chiefComplaint } } diff --git a/Intake/Home.swift b/Intake/Home.swift index e1400b8..f8ca07a 100644 --- a/Intake/Home.swift +++ b/Intake/Home.swift @@ -10,6 +10,14 @@ import SpeziAccount import SpeziMockWebService import SwiftUI +enum NavigationViews: String { + case allergies + case surgical + case medical + case social + case medication + case chat +} struct HomeView: View { enum Tabs: String { @@ -17,18 +25,32 @@ struct HomeView: View { case form case contact case mockUpload + case summary } static var accountEnabled: Bool { !FeatureFlags.disableFirebase && !FeatureFlags.skipOnboarding } - @AppStorage(StorageKeys.homeTabSelection) private var selectedTab = Tabs.schedule @State private var presentingAccount = false - + +// @State var navigationPath = NavigationPath() var body: some View { +// NavigationStack(path: $navigationPath) { +// LLMInteraction(presentingAccount: $presentingAccount) +// .navigationDestination(for: NavigationViews.self) { view in +// switch view { +// case .allergies: AllergyViewTest() +// default: SummaryView(chiefComplaint: "blah blah blah") +// // Fill in rest from NavigationView +// } +// } +// } +// .environmentObject(navigationPath) + + TabView(selection: $selectedTab) { ScheduleView(presentingAccount: $presentingAccount) .tag(Tabs.schedule) @@ -53,13 +75,13 @@ struct HomeView: View { Label("Create Form", systemImage: "captions.bubble.fill") } } - .sheet(isPresented: $presentingAccount) { - AccountSheet() - } - .accountRequired(Self.accountEnabled) { - AccountSheet() - } - .verifyRequiredAccountDetails(Self.accountEnabled) + .sheet(isPresented: $presentingAccount) { + AccountSheet() + } + .accountRequired(Self.accountEnabled) { + AccountSheet() + } + .verifyRequiredAccountDetails(Self.accountEnabled) } } diff --git a/Intake/IntakeDelegate.swift b/Intake/IntakeDelegate.swift index f2d3a43..07c8099 100644 --- a/Intake/IntakeDelegate.swift +++ b/Intake/IntakeDelegate.swift @@ -28,7 +28,7 @@ class IntakeDelegate: SpeziAppDelegate { AccountConfiguration(configuration: [ .requires(\.userId), .requires(\.name), - + // additional values stored using the `FirestoreAccountStorage` within our Standard implementation .collects(\.genderIdentity), .collects(\.dateOfBirth) @@ -77,28 +77,15 @@ class IntakeDelegate: SpeziAppDelegate { ) } - + // swiftlint:disable trailing_newline private var healthKit: HealthKit { HealthKit { - CollectSamples( - [ - HKClinicalType(.allergyRecord), - HKClinicalType(.clinicalNoteRecord), - HKClinicalType(.conditionRecord), - HKClinicalType(.coverageRecord), - HKClinicalType(.immunizationRecord), - HKClinicalType(.labResultRecord), - HKClinicalType(.medicationRecord), - HKClinicalType(.procedureRecord), - HKClinicalType(.vitalSignRecord) - ], - predicate: HKQuery.predicateForSamples( - withStart: Date.distantPast, - end: nil, - options: .strictEndDate - ), - deliverySetting: .anchorQuery(saveAnchor: false) + CollectSample( + HKQuantityType(.stepCount), + deliverySetting: .anchorQuery(.afterAuthorizationAndApplicationWillLaunch) ) } } } +// swiftlint:enable trailing newline + diff --git a/Intake/Resources/Localizable.xcstrings b/Intake/Resources/Localizable.xcstrings index 7ef4ec8..d398e6b 100644 --- a/Intake/Resources/Localizable.xcstrings +++ b/Intake/Resources/Localizable.xcstrings @@ -192,6 +192,9 @@ } } } + }, + "Here is a summary of your primary concern:" : { + }, "Integrate your Records" : { @@ -302,6 +305,9 @@ }, "Other important questions." : { + }, + "Primary Concern" : { + }, "PROJECT_LICENSE_DESCRIPTION" : { "localizations" : { @@ -372,17 +378,20 @@ }, "Share with provider of your choice." : { + }, + "Submit" : { + }, "Submit your Form" : { }, - "Summarize your medical history." : { + "Success!" : { }, - "Summarize your surgical history." : { + "Summarize your medical history." : { }, - "Summary" : { + "Summarize your surgical history." : { }, "Surgical History" : {