From 2700a05dbd622d9eca4afd7b292fbcd4c1c2215d Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Tue, 21 Nov 2023 18:42:41 +0100 Subject: [PATCH] Update Mock Patient Selection and other Bug Fixes --- .../FHIRResourceInterpreter.swift | 17 +++- .../FHIRResourceProcesser.swift | 2 +- .../FHIRResourceSummary.swift | 17 +++- .../Resources/Localizable.xcstrings | 91 ++++--------------- .../Settings/FHIRPrompt.swift | 40 ++++---- .../Settings/FHIRPromptSettingsView.swift | 4 +- .../FHIRBundle+MockPatients.swift | 87 ++++++++++++++---- .../FHIRBundleSelector.swift | 82 +++++++++++++++++ .../FHIRMockBundleSelector.swift | 40 ++++++++ .../FHIRMockPatientSelector.swift | 65 ------------- .../FoundationBundle+LoadBundle.swift | 8 +- Tests/UITests/TestApp/ContentView.swift | 74 +++++++++++++++ .../TestApp/MockPatientSelection.swift | 37 ++++++++ Tests/UITests/TestApp/PromptSettings.swift | 48 ++++++++++ Tests/UITests/TestApp/TestApp.swift | 2 +- .../UITests/UITests.xcodeproj/project.pbxproj | 12 +++ 16 files changed, 444 insertions(+), 182 deletions(-) create mode 100644 Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift create mode 100644 Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift delete mode 100644 Sources/SpeziFHIRMockPatients/FHIRMockPatientSelector.swift create mode 100644 Tests/UITests/TestApp/ContentView.swift create mode 100644 Tests/UITests/TestApp/MockPatientSelection.swift create mode 100644 Tests/UITests/TestApp/PromptSettings.swift diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift index 9a50c93..b58dc17 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceInterpreter.swift @@ -43,11 +43,22 @@ public class FHIRResourceInterpreter { extension FHIRPrompt { - static let interpretation: FHIRPrompt = { + /// Prompt used to interpret FHIR resources + /// + /// This prompt is used by the ``FHIRResourceInterpreter``. + public static let interpretation: FHIRPrompt = { FHIRPrompt( storageKey: "prompt.interpretation", - localizedDescription: String(localized: "Interpretation Prompt"), - defaultPrompt: String(localized: "FHIR_RESOURCE_INTERPRETATION_PROMPT \(FHIRPrompt.promptPlaceholder) \(Locale.preferredLanguages[0])") + localizedDescription: String( + localized: "Interpretation Prompt", + bundle: .module, + comment: "Title of the interpretation prompt." + ), + defaultPrompt: String( + localized: "Interpretation Prompt Content", + bundle: .module, + comment: "Content of the interpretation prompt." + ) ) }() } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift index 10ac5df..a3053be 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceProcesser.swift @@ -70,7 +70,7 @@ class FHIRResourceProcesser { private func systemPrompt(forResource resource: FHIRResource) -> Chat { Chat( role: .system, - content: prompt.prompt.replacingOccurrences(of: FHIRPrompt.promptPlaceholder, with: resource.jsonDescription) + content: prompt.prompt(withFHIRResource: resource.jsonDescription) ) } } diff --git a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift index 0471590..c161dbd 100644 --- a/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift +++ b/Sources/SpeziFHIRInterpretation/FHIRResourceSummary.swift @@ -43,11 +43,22 @@ public class FHIRResourceSummary { extension FHIRPrompt { - static let summary: FHIRPrompt = { + /// Prompt used to summarize FHIR resources + /// + /// This prompt is used by the ``FHIRResourceSummary``. + public static let summary: FHIRPrompt = { FHIRPrompt( storageKey: "prompt.summary", - localizedDescription: String(localized: "Summary Prompt"), - defaultPrompt: String(localized: "FHIR_RESOURCE_SUMMARY_PROMPT \(FHIRPrompt.promptPlaceholder) \(Locale.preferredLanguages[0])") + localizedDescription: String( + localized: "Summary Prompt", + bundle: .module, + comment: "Title of the summary prompt." + ), + defaultPrompt: String( + localized: "Summary Prompt Content", + bundle: .module, + comment: "Content of the summary prompt." + ) ) }() } diff --git a/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings b/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings index bad8489..5d22190 100644 --- a/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings +++ b/Sources/SpeziFHIRInterpretation/Resources/Localizable.xcstrings @@ -1,137 +1,86 @@ { "sourceLanguage" : "en", "strings" : { - "Chat with the user in the same language they chat in.\nChat with the user in %@" : { - "comment" : "The passed in string is the current locale of the device as a IETF BCP 47 language tag.", + "Customize the %@." : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Chat with the user in the same language they chat in.\nChat with the user in %@" + "value" : "Customize the %@." } } } }, - "Customize the %@ prompt." : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Customize the %@ prompt." - } - } - } - }, - "FHIR_MULTIPLE_RESOURCE_INTERPRETATION_PROMPT %@" : { - "extractionState" : "stale", + "Interpretation Prompt" : { + "comment" : "Title of the interpretation prompt.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your task is to interpret FHIR resources from the user's clinical records.\nThroughout the conversation with the user, use the get_resource_titles function to obtain the FHIR health resources necessary to properly answer the users question. For example, if the user asks about their allergies, you must use get_resource_titles to output the FHIR resource titles for allergy records so you can then use them to answer the question. The output of get_resource_titles has to be the name of a resource or resources with the exact same title as in the list provided.\n\nAnswer the users question, the health record provided is not always related. The end goal is to answer the users question in the best way possible.\n\nInterpret the resources by explaining its data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional.\nYou should provide factual and precise information in a compact summary in short responses.\n\nTell the user that they can ask any question about their health records and then create a short summary of the main categories of health records of the user which you have access to. These are the resource titles:\n%@\n\nImmediately return a short summary of the users health records to start the conversation.\nThe initial summary should be a short and simple summary with the following specifications:\n1. Short summary of the health records categories\n2. 5th grade reading level\n3. End with a question asking user if they have any questions. Make sure that this question is not generic but specific to their health records.\nDo not introduce yourself at the beginning, and start with your interpretation.\nMake sure your response is in the same language the user writes to you in.\nThe tense should be present." + "value" : "Interpretation Prompt" } } } }, - "FHIR_RESOURCE_INTERPRETATION_PROMPT %@" : { - "extractionState" : "stale", + "Interpretation Prompt Content" : { + "comment" : "Content of the interpretation prompt.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your task is to interpret the following FHIR resource from the user's clinical record.\n\nInterpret the resource by explaining its data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional.\nYou should provide factual and precise information in a compact summary in short responses.\n\nThe following JSON representation defines the FHIR resource that you should interpret:\n%@\n\nImmediately return an interpretation to the user, starting the conversation.\nDo not introduce yourself at the beginning, and start with your interpretation." - } - } - } - }, - "FHIR_RESOURCE_INTERPRETATION_PROMPT %@ %@" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "FHIR_RESOURCE_INTERPRETATION_PROMPT %1$@ %2$@" + "value" : "Your task is to interpret the following FHIR resource from the user's clinical record.\n\nInterpret the resource by explaining its data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional.\nYou should provide factual and precise information in a compact summary in short responses.\n\nThe following JSON representation defines the FHIR resource that you should interpret:\n{{FHIR_RESOURCE}}\n\nImmediately return an interpretation to the user, starting the conversation.\nDo not introduce yourself at the beginning, and start with your interpretation.\n\nChat with the user in the same language they chat in.\nChat with the user in {{LOCALE}}" } } } }, - "FHIR_RESOURCE_SUMMARY_PROMPT %@" : { - "extractionState" : "stale", + "Load Resource Summary" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Your task is to interpret the following FHIR resource from the user's clinical record.\n \nThe following JSON representation defines the FHIR resource that you should interpret:\n%@\n \nProvide a short one-sentence summary of the resource in less than 40 words. \nEnsure that the summary only focuses on the essential information that a patient would need and, e.g., excludes the person who prescribed a medication or similar metadata.\nDo NOT respond with more content than the single line containing the summary.\nDirectly provide the content without any additional structure." + "value" : "Load Resource Summary" } } } }, - "FHIR_RESOURCE_SUMMARY_PROMPT %@ %@" : { + "Place %@ at the position in the prompt where the FHIR resource should be inserted. Optionally place %@ where you would like to insert the current locale." : { "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "FHIR_RESOURCE_SUMMARY_PROMPT %1$@ %2$@" - } - } - } - }, - "Interpretation Prompt" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Interpretation Prompt" - } - } - } - }, - "Interpretation Prompt for Multiple Resources" : { - "extractionState" : "stale", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Interpretation Prompt for Multiple Resources" - } - } - } - }, - "Load Resource Summary" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Load Resource Summary" + "value" : "Place %1$@ at the position in the prompt where the FHIR resource should be inserted. Optionally place %2$@ where you would like to insert the current locale." } } } }, - "Place %@ at the position in the prompt where the FHIR resource should be inserted." : { + "Save Prompt" : { "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Place %@ at the position in the prompt where the FHIR resource should be inserted." + "value" : "Save Prompt" } } } }, - "Save Prompt" : { + "Summary Prompt" : { + "comment" : "Title of the summary prompt.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Save Prompt" + "value" : "Summary Prompt" } } } }, - "Summary Prompt" : { + "Summary Prompt Content" : { + "comment" : "Content of the summary prompt.", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Summary Prompt" + "value" : "Your task is to interpret FHIR resources from the user's clinical records.\nThroughout the conversation with the user, use the get_resource_titles function to obtain the FHIR health resources necessary to properly answer the users question. For example, if the user asks about their allergies, you must use get_resource_titles to output the FHIR resource titles for allergy records so you can then use them to answer the question. The output of get_resource_titles has to be the name of a resource or resources with the exact same title as in the list provided.\n\nAnswer the users question, the health record provided is not always related. The end goal is to answer the users question in the best way possible.\n\nInterpret the resources by explaining its data relevant to the user's health.\nExplain the relevant medical context in a language understandable by a user who is not a medical professional.\nYou should provide factual and precise information in a compact summary in short responses.\n\nTell the user that they can ask any question about their health records and then create a short summary of the main categories of health records of the user which you have access to. These are the resource titles:\n{{FHIR_RESOURCE}}\n\nImmediately return a short summary of the users health records to start the conversation.\nThe initial summary should be a short and simple summary with the following specifications:\n1. Short summary of the health records categories\n2. 5th grade reading level\n3. End with a question asking user if they have any questions. Make sure that this question is not generic but specific to their health records.\nDo not introduce yourself at the beginning, and start with your interpretation.\nMake sure your response is in the same language the user writes to you in.\nThe tense should be present. \n\nChat with the user in the same language they chat in.\nChat with the user in {{LOCALE}}" } } } diff --git a/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift b/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift index 515c1c2..64ffb1d 100644 --- a/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift +++ b/Sources/SpeziFHIRInterpretation/Settings/FHIRPrompt.swift @@ -10,9 +10,11 @@ import Foundation /// Handle dynamic, localized LLM prompts for FHIR resources. -public struct FHIRPrompt { - /// Placeholder for dynamic content in prompts. - public static let promptPlaceholder = "%@" +public struct FHIRPrompt: Hashable { + /// Placeholder for FHIR resource in prompts. + public static let fhirResourcePlaceholder = "{{FHIR_RESOURCE}}" + /// Placeholder for the current locale in a prompt + public static let localePlaceholder = "{{LOCALE}}" /// The key used for storing and retrieving the prompt. public let storageKey: String @@ -20,21 +22,10 @@ public struct FHIRPrompt { public let localizedDescription: String /// The default prompt text to be used if no custom prompt is set. public let defaultPrompt: String - + /// The current prompt, either from UserDefaults or the default, appended with a localized message that adapts to the user's language settings. public var prompt: String { - var prompt = UserDefaults.standard.string(forKey: storageKey) ?? defaultPrompt - - prompt += String( - localized: - """ - Chat with the user in the same language they chat in. - Chat with the user in \(Locale.preferredLanguages[0]) - """, - comment: "The passed in string is the current locale of the device as a IETF BCP 47 language tag." - ) - - return prompt + UserDefaults.standard.string(forKey: storageKey) ?? defaultPrompt } @@ -58,4 +49,21 @@ public struct FHIRPrompt { public func save(prompt: String) { UserDefaults.standard.set(prompt, forKey: storageKey) } + + public func hash(into hasher: inout Hasher) { + hasher.combine(storageKey) + } + + /// Creates a prompt based in the variable input. + /// + /// Use ``FHIRPrompt/fhirResourcePlaceholder`` and ``FHIRPrompt/localePlaceholder`` to define the elements that should be replaced. + /// - Parameters: + /// - resource: The resource that should be inserted in the prompt. + /// - locale: The current locale that should be inserted in the prompt. + /// - Returns: The constructed prompt. + public func prompt(withFHIRResource resource: String, locale: String = Locale.preferredLanguages[0]) -> String { + prompt + .replacingOccurrences(of: FHIRPrompt.fhirResourcePlaceholder, with: resource) + .replacingOccurrences(of: FHIRPrompt.localePlaceholder, with: locale) + } } diff --git a/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift b/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift index d4d6def..ef23542 100644 --- a/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift +++ b/Sources/SpeziFHIRInterpretation/Settings/FHIRPromptSettingsView.swift @@ -20,11 +20,11 @@ public struct PromptSettingsView: View { public var body: some View { VStack(spacing: 16) { - Text("Customize the \(promptType.localizedDescription.lowercased()) prompt.") + Text("Customize the \(promptType.localizedDescription.lowercased()).") .multilineTextAlignment(.leading) TextEditor(text: $prompt) .fontDesign(.monospaced) - Text("Place \(FHIRPrompt.promptPlaceholder) at the position in the prompt where the FHIR resource should be inserted.") + Text("Place \(FHIRPrompt.fhirResourcePlaceholder) at the position in the prompt where the FHIR resource should be inserted. Optionally place \(FHIRPrompt.localePlaceholder) where you would like to insert the current locale.") .multilineTextAlignment(.leading) .font(.caption) Button( diff --git a/Sources/SpeziFHIRMockPatients/FHIRBundle+MockPatients.swift b/Sources/SpeziFHIRMockPatients/FHIRBundle+MockPatients.swift index 45cf382..44801d3 100644 --- a/Sources/SpeziFHIRMockPatients/FHIRBundle+MockPatients.swift +++ b/Sources/SpeziFHIRMockPatients/FHIRBundle+MockPatients.swift @@ -11,29 +11,80 @@ import class ModelsR4.Bundle extension ModelsR4.Bundle { + private static var _jamison785Denesik803: ModelsR4.Bundle? /// Example FHIR resources packed into a bundle to represent the simulated patient named Jamison785 Denesik803. - public static let jamison785Denesik803: Bundle = Foundation.Bundle.module.loadFHIRBundle( - withName: "Jamison785_Denesik803_1e08cb3f-9e6a-b083-b6ee-0bb38f70ba50" - ) + public static var jamison785Denesik803: ModelsR4.Bundle { + get async { + if let jamison785Denesik803 = _jamison785Denesik803 { + return jamison785Denesik803 + } + + let jamison785Denesik803 = await Foundation.Bundle.module.loadFHIRBundle( + withName: "Jamison785_Denesik803_1e08cb3f-9e6a-b083-b6ee-0bb38f70ba50" + ) + ModelsR4.Bundle._jamison785Denesik803 = jamison785Denesik803 + return jamison785Denesik803 + } + } + + private static var _maye976Dickinson688: ModelsR4.Bundle? /// Example FHIR resources packed into a bundle to represent the simulated patient named Maye976 Dickinson688. - public static let maye976Dickinson688: Bundle = Foundation.Bundle.module.loadFHIRBundle( - withName: "Maye976_Dickinson688_04f25f73-04b2-469c-3806-540417a0d61c" - ) + public static var maye976Dickinson688: ModelsR4.Bundle { + get async { + if let maye976Dickinson688 = _maye976Dickinson688 { + return maye976Dickinson688 + } + + let maye976Dickinson688 = await Foundation.Bundle.module.loadFHIRBundle( + withName: "Maye976_Dickinson688_04f25f73-04b2-469c-3806-540417a0d61c" + ) + ModelsR4.Bundle._maye976Dickinson688 = maye976Dickinson688 + return maye976Dickinson688 + } + } + + private static var _milagros256Hills818: ModelsR4.Bundle? /// Example FHIR resources packed into a bundle to represent the simulated patient named Milagros256 Hills818. - public static let milagros256Hills818: Bundle = Foundation.Bundle.module.loadFHIRBundle( - withName: "Milagros256_Hills818_79b1d90a-0eaf-be78-9bbf-91c638626012" - ) + public static var milagros256Hills818: ModelsR4.Bundle { + get async { + if let milagros256Hills818 = _milagros256Hills818 { + return milagros256Hills818 + } + + let milagros256Hills818 = await Foundation.Bundle.module.loadFHIRBundle( + withName: "Milagros256_Hills818_79b1d90a-0eaf-be78-9bbf-91c638626012" + ) + ModelsR4.Bundle._milagros256Hills818 = milagros256Hills818 + return milagros256Hills818 + } + } + + private static var _napoleon578Fay398: ModelsR4.Bundle? /// Example FHIR resources packed into a bundle to represent the simulated patient named Napoleon578 Fay398. - public static let napoleon578Fay398: Bundle = Foundation.Bundle.module.loadFHIRBundle( - withName: "Napoleon578_Fay398_38f38890-b80f-6542-51d4-882c7b37b0bf" - ) + public static var napoleon578Fay398: ModelsR4.Bundle { + get async { + if let napoleon578Fay398 = _napoleon578Fay398 { + return napoleon578Fay398 + } + + let napoleon578Fay398 = await Foundation.Bundle.module.loadFHIRBundle( + withName: "Napoleon578_Fay398_38f38890-b80f-6542-51d4-882c7b37b0bf" + ) + ModelsR4.Bundle._napoleon578Fay398 = napoleon578Fay398 + return napoleon578Fay398 + } + } /// Loads example FHIR resources packed into a bundle to represent the simulated patients. - public static let mockPatients: [Bundle] = [ - .jamison785Denesik803, - .maye976Dickinson688, - .milagros256Hills818, - .napoleon578Fay398 - ] + public static var mockPatients: [Bundle] { + get async { + await [ + .jamison785Denesik803, + .maye976Dickinson688, + .milagros256Hills818, + .napoleon578Fay398 + ] + } + } } diff --git a/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift b/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift new file mode 100644 index 0000000..8181238 --- /dev/null +++ b/Sources/SpeziFHIRMockPatients/FHIRBundleSelector.swift @@ -0,0 +1,82 @@ +// +// This source file is part of the Stanford Spezi open source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import class ModelsR4.Bundle +import class ModelsR4.Patient +import SpeziFHIR +import SwiftUI + + +/// Loads resources from a FHIR bundle from a provided set of bundles. +/// +/// The View assumes that the bundle contains a `ModelsR4.Patient` resource to identify the bundle and provide a human-readable name. +public struct FHIRBundleSelector: View { + private struct PatientIdentifiedBundle: Identifiable { + let id: String + let bundle: ModelsR4.Bundle + } + + + private let bundles: [PatientIdentifiedBundle] + + @Environment(FHIRStore.self) private var store + + private var selectedBundle: Binding { + Binding( + get: { + guard let patient = store.otherResources.compactMap({ resource -> ModelsR4.Patient? in + guard case let .r4(resource) = resource.versionedResource, let loadedPatient = resource as? Patient else { + return nil + } + return loadedPatient + }).first else { + return nil + } + + + guard let bundle = bundles.first(where: { patient.identifier == $0.bundle.patient?.identifier }) else { + return nil + } + + return bundle.id + }, + set: { newValue in + guard let newValue, let bundle = bundles.first(where: { $0.id == newValue })?.bundle else { + return + } + + store.removeAllResources() + store.load(bundle: bundle) + } + ) + } + + + public var body: some View { + Picker( + String(localized: "Select Mock Patient", bundle: .module), + selection: selectedBundle + ) { + ForEach(bundles) { bundle in + Text(bundle.bundle.patientName) + .tag(bundle.id as String?) + } + } + } + + + public init(bundles: [ModelsR4.Bundle]) { + self.bundles = bundles.compactMap { + guard let id = $0.patient?.identifier?.first?.value?.value?.string else { + return nil + } + + return PatientIdentifiedBundle(id: id, bundle: $0) + } + } +} diff --git a/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift b/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift new file mode 100644 index 0000000..5c2f7f4 --- /dev/null +++ b/Sources/SpeziFHIRMockPatients/FHIRMockBundleSelector.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +@preconcurrency import ModelsR4 +import SwiftUI + + +/// Loads resources from a FHIR bundle from a provided set of mock bundles defined as an extension on `ModelsR4.Bundle`. +/// +/// The View assumes that the bundle contains a `ModelsR4.Patient` resource to identify the bundle and provide a human-readable name. +public struct FHIRMockPatientSelection: View { + @State var bundles: [ModelsR4.Bundle] = [] + + + public var body: some View { + Group { + if bundles.isEmpty { + HStack { + Spacer() + ProgressView() + Spacer() + } + } else { + FHIRBundleSelector(bundles: bundles) + .pickerStyle(.inline) + } + } + .task { + self.bundles = await ModelsR4.Bundle.mockPatients + } + } + + + public init() { } +} diff --git a/Sources/SpeziFHIRMockPatients/FHIRMockPatientSelector.swift b/Sources/SpeziFHIRMockPatients/FHIRMockPatientSelector.swift deleted file mode 100644 index dd19825..0000000 --- a/Sources/SpeziFHIRMockPatients/FHIRMockPatientSelector.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// This source file is part of the Stanford Spezi open source project -// -// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import class ModelsR4.Bundle -import class ModelsR4.Patient -import SpeziFHIR -import SwiftUI - - -/// Loads resources from a FHIR bundle from a provided set of bundles. -/// -/// The View assumes that the bundle contains a `ModelsR4.Patient` resource to identify the bundle and provide a human-readable name. -public struct FHIRMockPatientSelector: View { - @Environment(FHIRStore.self) var store - - let bundles: [ModelsR4.Bundle] - - - @State var selectedBundle: ModelsR4.Bundle? { - didSet { - guard let selectedBundle else { - return - } - - store.removeAllResources() - store.load(bundle: selectedBundle) - } - } - - public var body: some View { - Picker( - String(localized: "Select Mock Patient", bundle: .module), - selection: $selectedBundle - ) { - ForEach(bundles) { bundle in - Text(bundle.patientName) - .tag(bundle) - } - } - .onAppear { - let patient = store.otherResources.compactMap { resource -> ModelsR4.Patient? in - guard case let .r4(resource) = resource.versionedResource, let loadedPatient = resource as? Patient else { - return nil - } - return loadedPatient - }.first - - - for bundle in bundles where patient?.identifier == bundle.patient?.identifier { - selectedBundle = bundle - return - } - } - } - - - public init(bundles: [ModelsR4.Bundle] = Bundle.mockPatients) { - self.bundles = bundles - } -} diff --git a/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift b/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift index 99a2819..5768279 100644 --- a/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift +++ b/Sources/SpeziFHIRMockPatients/FoundationBundle+LoadBundle.swift @@ -14,14 +14,18 @@ extension Foundation.Bundle { /// Loads a FHIR `Bundle` from a Foundation `Bundle`. /// - Parameter name: Name of the JSON file in the Foundation `Bundle` /// - Returns: The FHIR `Bundle` - public func loadFHIRBundle(withName name: String) -> Bundle { + public func loadFHIRBundle(withName name: String) async -> Bundle { guard let resourceURL = self.url(forResource: name, withExtension: "json") else { fatalError("Could not find the resource \"\(name)\".json in the SpeziFHIRMockPatients Resources folder.") } - do { + let loadingTask = Task { let resourceData = try Data(contentsOf: resourceURL) return try JSONDecoder().decode(Bundle.self, from: resourceData) + } + + do { + return try await loadingTask.value } catch { fatalError("Could not decode the FHIR bundle named \"\(name).json\": \(error)") } diff --git a/Tests/UITests/TestApp/ContentView.swift b/Tests/UITests/TestApp/ContentView.swift new file mode 100644 index 0000000..97f8c92 --- /dev/null +++ b/Tests/UITests/TestApp/ContentView.swift @@ -0,0 +1,74 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIR +import SwiftUI + + +struct ContentView: View { + @Environment(FHIRStore.self) var fhirStore + @State var presentPatientSelection = false + @State var presentPromptSettings = false + + + var body: some View { + NavigationStack { + List { + Section { + Text("Allergy Intolerances: \(fhirStore.allergyIntolerances.count)") + Text("Conditions: \(fhirStore.conditions.count)") + Text("Diagnostics: \(fhirStore.diagnostics.count)") + Text("Encounters: \(fhirStore.encounters.count)") + Text("Immunizations: \(fhirStore.immunizations.count)") + Text("Medications: \(fhirStore.medications.count)") + Text("Observations: \(fhirStore.observations.count)") + Text("Other Resources: \(fhirStore.otherResources.count)") + Text("Procedures: \(fhirStore.procedures.count)") + } + Section { + presentPatientSelectionButton + } + } + .sheet(isPresented: $presentPatientSelection) { + MockPatientSelection(presentPatientSelection: $presentPatientSelection) + } + .sheet(isPresented: $presentPromptSettings) { + PromptSettings(presentPromptSettings: $presentPromptSettings) + } + .toolbar { + ToolbarItem { + presentPromptSettingsButton + } + } + } + } + + + @ViewBuilder private var presentPromptSettingsButton: some View { + Button( + action: { + presentPromptSettings.toggle() + }, + label: { + Image(systemName: "gear") + .accessibilityLabel(Text("Settings")) + } + ) + } + + @ViewBuilder private var presentPatientSelectionButton: some View { + Button( + action: { + presentPatientSelection.toggle() + }, + label: { + Text("Select Mock Patient") + } + ) + } +} diff --git a/Tests/UITests/TestApp/MockPatientSelection.swift b/Tests/UITests/TestApp/MockPatientSelection.swift new file mode 100644 index 0000000..1604874 --- /dev/null +++ b/Tests/UITests/TestApp/MockPatientSelection.swift @@ -0,0 +1,37 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIRMockPatients +import SwiftUI + + +struct MockPatientSelection: View { + @Binding var presentPatientSelection: Bool + + + var body: some View { + NavigationStack { + List { + FHIRMockPatientSelection() + } + .toolbar { + ToolbarItem { + Button( + action: { + presentPatientSelection.toggle() + }, + label: { + Text("Dismiss") + } + ) + } + } + .navigationTitle("Select Mock Patient") + } + } +} diff --git a/Tests/UITests/TestApp/PromptSettings.swift b/Tests/UITests/TestApp/PromptSettings.swift new file mode 100644 index 0000000..0cb616e --- /dev/null +++ b/Tests/UITests/TestApp/PromptSettings.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Stanford Spezi open-source project +// +// SPDX-FileCopyrightText: 2023 Stanford University and the project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import SpeziFHIRInterpretation +import SwiftUI + + +struct PromptSettings: View { + @Binding var presentPromptSettings: Bool + @State var prompt: FHIRPrompt? + + + var body: some View { + NavigationStack { + List { + NavigationLink(value: FHIRPrompt.summary) { + Text(FHIRPrompt.summary.localizedDescription) + } + NavigationLink(value: FHIRPrompt.interpretation) { + Text(FHIRPrompt.interpretation.localizedDescription) + } + } + .navigationDestination(for: FHIRPrompt.self) { prompt in + PromptSettingsView(promptType: prompt) { + print("Saved \(prompt.localizedDescription)") + } + } + .toolbar { + ToolbarItem { + Button( + action: { + presentPromptSettings.toggle() + }, + label: { + Text("Dismiss") + } + ) + } + } + .navigationTitle("Prompt Settings") + } + } +} diff --git a/Tests/UITests/TestApp/TestApp.swift b/Tests/UITests/TestApp/TestApp.swift index 3a511fc..c481a09 100644 --- a/Tests/UITests/TestApp/TestApp.swift +++ b/Tests/UITests/TestApp/TestApp.swift @@ -17,7 +17,7 @@ struct UITestsApp: App { var body: some Scene { WindowGroup { - Text("Spezi FHIR") + ContentView() .spezi(appDelegate) } } diff --git a/Tests/UITests/UITests.xcodeproj/project.pbxproj b/Tests/UITests/UITests.xcodeproj/project.pbxproj index 874c1c9..851afb7 100644 --- a/Tests/UITests/UITests.xcodeproj/project.pbxproj +++ b/Tests/UITests/UITests.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 2F34D14E2B0CF1F1009300C1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F34D14D2B0CF1F1009300C1 /* ContentView.swift */; }; + 2F34D1502B0CF42F009300C1 /* PromptSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F34D14F2B0CF42F009300C1 /* PromptSettings.swift */; }; + 2F34D1522B0CF59A009300C1 /* MockPatientSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F34D1512B0CF59A009300C1 /* MockPatientSelection.swift */; }; 2F35E9D62B015EB200CB89FF /* SpeziFHIRInterpretation in Frameworks */ = {isa = PBXBuildFile; productRef = 2F35E9D52B015EB200CB89FF /* SpeziFHIRInterpretation */; }; 2F36AD33299DB72400B1077C /* FHIRMockDataStorageProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F36AD32299DB72400B1077C /* FHIRMockDataStorageProviderTests.swift */; }; 2F6D139A28F5F386007C25D6 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2F6D139928F5F386007C25D6 /* Assets.xcassets */; }; @@ -28,6 +31,9 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 2F34D14D2B0CF1F1009300C1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2F34D14F2B0CF42F009300C1 /* PromptSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptSettings.swift; sourceTree = ""; }; + 2F34D1512B0CF59A009300C1 /* MockPatientSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPatientSelection.swift; sourceTree = ""; }; 2F36AD32299DB72400B1077C /* FHIRMockDataStorageProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRMockDataStorageProviderTests.swift; sourceTree = ""; }; 2F6D139228F5F384007C25D6 /* TestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2F6D139928F5F386007C25D6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -84,6 +90,9 @@ 2F6D139428F5F384007C25D6 /* TestApp */ = { isa = PBXGroup; children = ( + 2F34D14D2B0CF1F1009300C1 /* ContentView.swift */, + 2F34D1512B0CF59A009300C1 /* MockPatientSelection.swift */, + 2F34D14F2B0CF42F009300C1 /* PromptSettings.swift */, 2FA7382B290ADFAA007ACEB9 /* TestApp.swift */, 2FD021DC299E0F2900E5B91B /* TestAppDelegate.swift */, 2F6D139928F5F386007C25D6 /* Assets.xcassets */, @@ -239,6 +248,9 @@ files = ( 2FA7382C290ADFAA007ACEB9 /* TestApp.swift in Sources */, 2FD021DD299E0F2900E5B91B /* TestAppDelegate.swift in Sources */, + 2F34D1502B0CF42F009300C1 /* PromptSettings.swift in Sources */, + 2F34D1522B0CF59A009300C1 /* MockPatientSelection.swift in Sources */, + 2F34D14E2B0CF1F1009300C1 /* ContentView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };