Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patient generalData flow refractor and PatientInfo View #64

Merged
merged 14 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Intake.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
2FE5DCB129EE6107004B9AB4 /* AccountOnboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */; };
2FF53D8B2A8725DE00042B76 /* SpeziMockWebService in Frameworks */ = {isa = PBXBuildFile; productRef = 2FF53D8A2A8725DE00042B76 /* SpeziMockWebService */; };
2FF53D8D2A8729D600042B76 /* IntakeStandard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */; };
3C89F66D2B9D948B00A4F52D /* PatientInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C89F66C2B9D948B00A4F52D /* PatientInfo.swift */; };
511827962B740192002033A0 /* SurgeryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511827952B740191002033A0 /* SurgeryView.swift */; };
51805C122B81853800D17109 /* IntakeMedication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C112B81853700D17109 /* IntakeMedication.swift */; };
51805C152B81857100D17109 /* IntakeMedicationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */; };
Expand Down Expand Up @@ -175,6 +176,7 @@
2FE5DC5529EDD811004B9AB4 /* SocialSupportQuestionnaire.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = SocialSupportQuestionnaire.json; sourceTree = "<group>"; };
2FE5DCAC29EE6107004B9AB4 /* AccountOnboarding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountOnboarding.swift; sourceTree = "<group>"; };
2FF53D8C2A8729D600042B76 /* IntakeStandard.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntakeStandard.swift; sourceTree = "<group>"; };
3C89F66C2B9D948B00A4F52D /* PatientInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatientInfo.swift; sourceTree = "<group>"; };
511827952B740191002033A0 /* SurgeryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SurgeryView.swift; sourceTree = "<group>"; };
51805C112B81853700D17109 /* IntakeMedication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedication.swift; sourceTree = "<group>"; };
51805C142B81857100D17109 /* IntakeMedicationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntakeMedicationViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -380,6 +382,14 @@
path = Helper;
sourceTree = "<group>";
};
3C89F6682B9D939500A4F52D /* General Data View */ = {
isa = PBXGroup;
children = (
3C89F66C2B9D948B00A4F52D /* PatientInfo.swift */,
);
path = "General Data View";
sourceTree = "<group>";
};
511827942B740191002033A0 /* Surgery */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -499,6 +509,7 @@
653A254F283387FE005D4D48 /* Intake */ = {
isa = PBXGroup;
children = (
3C89F6682B9D939500A4F52D /* General Data View */,
F4F4F8802B8C6FC5008FBEED /* Elements.swift */,
519E830A2B7C4F1600A2D92D /* Medication View */,
511827942B740191002033A0 /* Surgery */,
Expand Down Expand Up @@ -826,6 +837,7 @@
F42AB1DF2B637C9D002E13A6 /* LLMInteraction.swift in Sources */,
51A360162B965819004E7E12 /* AllergyLLMAssistant.swift in Sources */,
F42AB1F22B71B4D2002E13A6 /* AllergyViewTest.swift in Sources */,
3C89F66D2B9D948B00A4F52D /* PatientInfo.swift in Sources */,
2FE5DC3829EDD7CA004B9AB4 /* InterestingModules.swift in Sources */,
2FE5DC3529EDD7CA004B9AB4 /* Consent.swift in Sources */,
51A360182B9659AE004E7E12 /* MedicalHistoryLLMAssistant.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordBDHG/ResearchKit",
"state" : {
"revision" : "e4afb10ec636a70e06afc4a84fb98e457818439a",
"version" : "2.2.27"
"revision" : "64512d0a0a5cc3e9d5b3fc5217c54f11d0dc044c",
"version" : "2.2.28"
}
},
{
Expand Down Expand Up @@ -275,8 +275,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/StanfordSpezi/SpeziQuestionnaire.git",
"state" : {
"revision" : "f25580e95bfdad02383980dcb94406cf97b08ea8",
"version" : "1.0.2"
"revision" : "f9d9b6d99bb1e00bda2974b440dca8367733d591",
"version" : "1.1.0"
}
},
{
Expand Down
200 changes: 102 additions & 98 deletions Intake/ChiefComplaint/LLMInteraction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,71 +19,27 @@
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
}
struct LLMInteraction: View {
// swiftlint:disable type_contents_order
@State private var fullName: String = ""
@State private var firstName: String = ""
@State private var dob: String = ""
@State private var gender: String = ""
@Environment(LLMRunner.self) var runner: LLMRunner
@Environment(FHIRStore.self) private var fhirStore
@Environment(DataStore.self) private var data
@Environment(NavigationPathWrapper.self) private var navigationPath

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
}
@Binding var presentingAccount: Bool
@LLMSessionProvider<LLMOpenAISchema> var session: LLMOpenAISession

func getInfo(patient: FHIRResource, field: String) -> String {
let jsonDescription = patient.jsonDescription
@State var showOnboarding = true
@State var greeting = true

if let infoValue = getValue(forKey: field, from: jsonDescription) {
print("Info found: \(infoValue)")
return infoValue
}
@State var stringBox: StringBox = .init()
@State var showSheet = false

print("Key \(field) not found")
return ""
}


struct LLMInteraction: View {
@Observable
class StringBox: Equatable {
var llmResponseSummary: String
Expand All @@ -96,6 +52,69 @@
lhs.llmResponseSummary == rhs.llmResponseSummary
}
}

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"
}
}

Check warning on line 73 in Intake/ChiefComplaint/LLMInteraction.swift

View check run for this annotation

Codecov / codecov/patch

Intake/ChiefComplaint/LLMInteraction.swift#L56-L73

Added lines #L56 - L73 were not covered by tests

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
}

Check warning on line 105 in Intake/ChiefComplaint/LLMInteraction.swift

View check run for this annotation

Codecov / codecov/patch

Intake/ChiefComplaint/LLMInteraction.swift#L75-L105

Added lines #L75 - L105 were not covered by tests

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 ""
}

Check warning on line 117 in Intake/ChiefComplaint/LLMInteraction.swift

View check run for this annotation

Codecov / codecov/patch

Intake/ChiefComplaint/LLMInteraction.swift#L107-L117

Added lines #L107 - L117 were not covered by tests

struct SummarizeFunction: LLMFunction {
static let name: String = "summarize_complaint"
Expand Down Expand Up @@ -123,20 +142,6 @@
return nil
}
}

@Environment(LLMRunner.self) var runner: LLMRunner
@Environment(FHIRStore.self) private var fhirStore
@Environment(DataStore.self) private var data
@Environment(NavigationPathWrapper.self) private var navigationPath

@Binding var presentingAccount: Bool
@LLMSessionProvider<LLMOpenAISchema> var session: LLMOpenAISession

@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
Expand All @@ -154,37 +159,17 @@
}

.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: " ")

data.generalData.name = fullName
data.generalData.birthdate = dob
data.generalData.sex = gender
data.generalData.age = age

firstName = nameString.first ?? "First Name is empty"
print(firstName == "First Name is empty" ? "First Name is empty" : "")


loadData()
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 first name of the patient is \(String(describing: firstName)) and the patient is \(String(describing: data.generalData.age)) \
years old. The patient's sex is \(String(describing: data.generalData.sex)) 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 {
if firstName.isEmpty {
Expand Down Expand Up @@ -221,6 +206,25 @@
private func showSummary() {
navigationPath.path.append(NavigationViews.concern)
}

private func loadData() {
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")
}

data.generalData = PatientData(name: fullName, birthdate: dob, age: age, sex: gender)

Check warning on line 225 in Intake/ChiefComplaint/LLMInteraction.swift

View check run for this annotation

Codecov / codecov/patch

Intake/ChiefComplaint/LLMInteraction.swift#L212-L225

Added lines #L212 - L225 were not covered by tests
}
}
}

#Preview {
Expand Down
34 changes: 18 additions & 16 deletions Intake/EditPatient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,35 @@
//

import Foundation
import SpeziFHIR
import SwiftUI


struct EditPatientView: View {
@Environment(DataStore.self) private var data
@Environment(FHIRStore.self) private var fhirStore

var body: some View {
@Bindable var data = data
VStack {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $data.generalData.name)
}
Section(header: Text("Date of Birth")) {
TextField("Date of Birth", text: $data.generalData.birthdate)
}
Section(header: Text("Age")) {
TextField("Age", text: $data.generalData.age)
}
Section(header: Text("Sex")) {
TextField("Sex", text: $data.generalData.sex)

VStack {
Form {
Section(header: Text("Name")) {
TextField("Name", text: $data.generalData.name)
}
Section(header: Text("Date of Birth")) {
TextField("Date of Birth", text: $data.generalData.birthdate)
}
Section(header: Text("Age")) {
TextField("Age", text: $data.generalData.age)
}
Section(header: Text("Sex")) {
TextField("Sex", text: $data.generalData.sex)
}

Check warning on line 38 in Intake/EditPatient.swift

View check run for this annotation

Codecov / codecov/patch

Intake/EditPatient.swift#L24-L38

Added lines #L24 - L38 were not covered by tests
}
SubmitButton(nextView: NavigationViews.pdfs)

Check warning on line 40 in Intake/EditPatient.swift

View check run for this annotation

Codecov / codecov/patch

Intake/EditPatient.swift#L40

Added line #L40 was not covered by tests
}
SubmitButton(nextView: NavigationViews.pdfs)
}
}
}

//
// #Preview {
Expand Down
Loading
Loading