Skip to content

Commit

Permalink
Improve List Overview, Add Summary to Detail View, Add Settings Scree…
Browse files Browse the repository at this point in the history
…n, and Modify Prompt Settings (#18)
  • Loading branch information
PSchmiedmayer authored May 24, 2023
1 parent 11d9eed commit 45f99cc
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 107 deletions.
28 changes: 24 additions & 4 deletions LLMonFHIR.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
objects = {

/* Begin PBXBuildFile section */
2F0F90BA2A1E6556006D7E03 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0F90B92A1E6556006D7E03 /* SettingsView.swift */; };
2F0F90BC2A1E6B19006D7E03 /* PromptSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0F90BB2A1E6B19006D7E03 /* PromptSettingsView.swift */; };
2F0F90BE2A1E702B006D7E03 /* Prompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F0F90BD2A1E702B006D7E03 /* Prompt.swift */; };
2F49B7762980407C00BCB272 /* Spezi in Frameworks */ = {isa = PBXBuildFile; productRef = 2F49B7752980407B00BCB272 /* Spezi */; };
2F4E237E2989A2FE0013F3D9 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */; };
2F4E23832989D51F0013F3D9 /* LLMonFHIRTestingSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F4E23822989D51F0013F3D9 /* LLMonFHIRTestingSetup.swift */; };
Expand All @@ -23,7 +26,7 @@
2FD8E82C2A1AADDA00357F4E /* ModelsR4 in Frameworks */ = {isa = PBXBuildFile; productRef = 2FD8E82B2A1AADDA00357F4E /* ModelsR4 */; };
2FD8E82E2A1AAFC300357F4E /* HealthKitToFHIRAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD8E82D2A1AAFC300357F4E /* HealthKitToFHIRAdapter.swift */; };
2FD8E8312A1AB00D00357F4E /* HealthKitOnFHIR in Frameworks */ = {isa = PBXBuildFile; productRef = 2FD8E8302A1AB00D00357F4E /* HealthKitOnFHIR */; };
2FD8E8332A1AB68E00357F4E /* VersionedResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD8E8322A1AB68E00357F4E /* VersionedResource.swift */; };
2FD8E8332A1AB68E00357F4E /* FHIRResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD8E8322A1AB68E00357F4E /* FHIRResource.swift */; };
2FD8E83B2A1AD10300357F4E /* FHIRResourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD8E83A2A1AD10300357F4E /* FHIRResourcesView.swift */; };
2FD8E83D2A1AE24F00357F4E /* InspectResourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FD8E83C2A1AE24F00357F4E /* InspectResourceView.swift */; };
2FD8E8402A1AE3F200357F4E /* SpeziViews in Frameworks */ = {isa = PBXBuildFile; productRef = 2FD8E83F2A1AE3F200357F4E /* SpeziViews */; };
Expand Down Expand Up @@ -64,6 +67,9 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
2F0F90B92A1E6556006D7E03 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
2F0F90BB2A1E6B19006D7E03 /* PromptSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromptSettingsView.swift; sourceTree = "<group>"; };
2F0F90BD2A1E702B006D7E03 /* Prompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prompt.swift; sourceTree = "<group>"; };
2F4E237D2989A2FE0013F3D9 /* OnboardingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTests.swift; sourceTree = "<group>"; };
2F4E23822989D51F0013F3D9 /* LLMonFHIRTestingSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LLMonFHIRTestingSetup.swift; sourceTree = "<group>"; };
2F55FC522A1AE8750051DF48 /* FHIRResourceInterpreter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRResourceInterpreter.swift; sourceTree = "<group>"; };
Expand All @@ -77,7 +83,7 @@
2FC975A72978F11A00BA99FE /* Home.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Home.swift; sourceTree = "<group>"; };
2FD8E8262A1AAD9B00357F4E /* FHIR.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FHIR.swift; sourceTree = "<group>"; };
2FD8E82D2A1AAFC300357F4E /* HealthKitToFHIRAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HealthKitToFHIRAdapter.swift; sourceTree = "<group>"; };
2FD8E8322A1AB68E00357F4E /* VersionedResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionedResource.swift; sourceTree = "<group>"; };
2FD8E8322A1AB68E00357F4E /* FHIRResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRResource.swift; sourceTree = "<group>"; };
2FD8E83A2A1AD10300357F4E /* FHIRResourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FHIRResourcesView.swift; sourceTree = "<group>"; };
2FD8E83C2A1AE24F00357F4E /* InspectResourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectResourceView.swift; sourceTree = "<group>"; };
2FE5DC3029EDD7CA004B9AB4 /* HealthKitPermissions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HealthKitPermissions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -134,6 +140,16 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
2F0F90B82A1E6542006D7E03 /* Settings */ = {
isa = PBXGroup;
children = (
2F0F90B92A1E6556006D7E03 /* SettingsView.swift */,
2F0F90BB2A1E6B19006D7E03 /* PromptSettingsView.swift */,
2F0F90BD2A1E702B006D7E03 /* Prompt.swift */,
);
path = Settings;
sourceTree = "<group>";
};
2FC9759D2978E30800BA99FE /* Supporting Files */ = {
isa = PBXGroup;
children = (
Expand All @@ -147,7 +163,7 @@
isa = PBXGroup;
children = (
2FD8E8262A1AAD9B00357F4E /* FHIR.swift */,
2FD8E8322A1AB68E00357F4E /* VersionedResource.swift */,
2FD8E8322A1AB68E00357F4E /* FHIRResource.swift */,
2FD8E82D2A1AAFC300357F4E /* HealthKitToFHIRAdapter.swift */,
);
path = "FHIR Standard";
Expand Down Expand Up @@ -238,6 +254,7 @@
2FE5DC2829EDD398004B9AB4 /* Onboarding */,
2FD8E81D2A1AAC6900357F4E /* FHIR Standard */,
2FD8E8392A1AD0E900357F4E /* FHIR Display */,
2F0F90B82A1E6542006D7E03 /* Settings */,
2FE5DC3C29EDD7DA004B9AB4 /* SharedContext */,
2FE5DC3D29EDD7E4004B9AB4 /* Helper */,
2FE5DC2D29EDD792004B9AB4 /* Resources */,
Expand Down Expand Up @@ -452,11 +469,14 @@
2F61A3302A1B51BF000A7796 /* FHIRResourceSummary.swift in Sources */,
2FE5DC3A29EDD7CA004B9AB4 /* Welcome.swift in Sources */,
2FE5DC3829EDD7CA004B9AB4 /* Disclaimer.swift in Sources */,
2FD8E8332A1AB68E00357F4E /* VersionedResource.swift in Sources */,
2FD8E8332A1AB68E00357F4E /* FHIRResource.swift in Sources */,
2F0F90BC2A1E6B19006D7E03 /* PromptSettingsView.swift in Sources */,
2F61A3322A1B560B000A7796 /* ResourceSummaryView.swift in Sources */,
2FD8E83D2A1AE24F00357F4E /* InspectResourceView.swift in Sources */,
2F0F90BA2A1E6556006D7E03 /* SettingsView.swift in Sources */,
2FD8E83B2A1AD10300357F4E /* FHIRResourcesView.swift in Sources */,
2FD8E82E2A1AAFC300357F4E /* HealthKitToFHIRAdapter.swift in Sources */,
2F0F90BE2A1E702B006D7E03 /* Prompt.swift in Sources */,
2FE5DC4529EDD7F2004B9AB4 /* Binding+Negate.swift in Sources */,
2FC975A82978F11A00BA99FE /* Home.swift in Sources */,
2FE5DC3729EDD7CA004B9AB4 /* OnboardingFlow.swift in Sources */,
Expand Down
10 changes: 5 additions & 5 deletions LLMonFHIR/FHIR Display/FHIRResourceInterpreter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private enum FHIRResourceInterpreterConstants {


class FHIRResourceInterpreter<ComponentStandard: Standard>: DefaultInitializable, Component, ObservableObject, ObservableObjectProvider {
typealias Interpretations = [VersionedResource.ID: String]
typealias Interpretations = [FHIRResource.ID: String]


@Dependency private var localStorage: LocalStorage
Expand Down Expand Up @@ -54,7 +54,7 @@ class FHIRResourceInterpreter<ComponentStandard: Standard>: DefaultInitializable
}


func interpret(resource: VersionedResource) async throws {
func interpret(resource: FHIRResource) async throws {
let chatStreamResults = try await openAIComponent.queryAPI(withChat: [systemPrompt(forResource: resource)])

self.interpretations[resource.id] = ""
Expand All @@ -67,18 +67,18 @@ class FHIRResourceInterpreter<ComponentStandard: Standard>: DefaultInitializable
}
}

func chat(forResource resource: VersionedResource) -> [Chat] {
func chat(forResource resource: FHIRResource) -> [Chat] {
var chat = [systemPrompt(forResource: resource)]
if let interpretation = interpretations[resource.id] {
chat.append(Chat(role: .assistant, content: interpretation))
}
return chat
}

private func systemPrompt(forResource resource: VersionedResource) -> Chat {
private func systemPrompt(forResource resource: FHIRResource) -> Chat {
Chat(
role: .system,
content: String(localized: "FHIR_RESOURCE_INTERPRETATION_PROMPT \(resource.jsonDescription)")
content: Prompt.interpretation.prompt.replacingOccurrences(of: Prompt.promptPlaceholder, with: resource.compactJSONDescription)
)
}
}
18 changes: 7 additions & 11 deletions LLMonFHIR/FHIR Display/FHIRResourceSummary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,33 +19,30 @@ private enum FHIRResourceSummaryConstants {


class FHIRResourceSummary<ComponentStandard: Standard>: DefaultInitializable, Component, ObservableObject, ObservableObjectProvider {
typealias Summaries = [VersionedResource.ID: FHIRResourceSummary]
typealias Summaries = [FHIRResource.ID: FHIRResourceSummary]


struct FHIRResourceSummary: LosslessStringConvertible, Codable {
init?(_ description: String) {
let lines = description.split(whereSeparator: \.isNewline)

guard lines.count == 2, let title = lines.first, let summary = lines.last else {
guard lines.count == 1, let summary = lines.first else {
return nil
}

self.title = String(title)
self.summary = String(summary)
}

init(title: String, summary: String) {
self.title = title
init(summary: String) {
self.summary = summary
}


let title: String
let summary: String


var description: String {
"\(title)\n\(summary)"
summary
}
}

Expand Down Expand Up @@ -81,8 +78,7 @@ class FHIRResourceSummary<ComponentStandard: Standard>: DefaultInitializable, Co
self.summaries = cachedSummaries
}


func summarize(resource: VersionedResource) async throws {
func summarize(resource: FHIRResource) async throws {
guard summaries[resource.id] == nil else {
return
}
Expand All @@ -101,10 +97,10 @@ class FHIRResourceSummary<ComponentStandard: Standard>: DefaultInitializable, Co
}


private func systemPrompt(forResource resource: VersionedResource) -> Chat {
private func systemPrompt(forResource resource: FHIRResource) -> Chat {
Chat(
role: .system,
content: String(localized: "FHIR_RESOURCE_SUMMARY_PROMPT \(resource.jsonDescription)")
content: Prompt.summary.prompt.replacingOccurrences(of: Prompt.promptPlaceholder, with: resource.compactJSONDescription)
)
}
}
13 changes: 6 additions & 7 deletions LLMonFHIR/FHIR Display/FHIRResourcesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import SwiftUI

struct FHIRResourcesView: View {
@EnvironmentObject var fhirStandard: FHIR
@State var resources: [String: [VersionedResource]] = [:]
@State var resources: [String: [FHIRResource]] = [:]
@State var showSettings = false
@AppStorage(StorageKeys.onboardingInstructions) var onboardingInstructions = true

Expand All @@ -27,7 +27,7 @@ struct FHIRResourcesView: View {
}
}
}
.navigationDestination(for: VersionedResource.self) { resource in
.navigationDestination(for: FHIRResource.self) { resource in
InspecResourceView(resource: resource)
}
.onReceive(fhirStandard.objectWillChange) {
Expand All @@ -46,9 +46,7 @@ struct FHIRResourcesView: View {
}
}
.sheet(isPresented: $showSettings) {
OpenAIAPIKeyOnboardingStep<FHIR>(actionText: String(localized: "OPEN_AI_KEY_SAVE_ACTION")) {
showSettings.toggle()
}
SettingsView()
}
.navigationTitle("FHIR_RESOURCES_TITLE")
}
Expand Down Expand Up @@ -106,8 +104,9 @@ struct FHIRResourcesView: View {
}

private func loadFHIRResources() {
Task {
let resources = await Array(fhirStandard.resources.values)
Task { @MainActor in
let values = await fhirStandard.resources.values
let resources = Array(values)
self.resources = [:]
for resource in resources {
var currentResources = self.resources[resource.resourceType, default: []]
Expand Down
4 changes: 2 additions & 2 deletions LLMonFHIR/FHIR Display/InspectResourceChat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ struct InspectResourceChat: View {
@State var chat: [Chat]
@State var gettingAnswer = false

let resource: VersionedResource
let resource: FHIRResource


var body: some View {
NavigationStack {
ChatView($chat, disableInput: $gettingAnswer)
.navigationTitle(fhirResourceSummary.summaries[resource.id]?.title ?? resource.compactDescription)
.navigationTitle(resource.displayName)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("FHIR_RESOURCES_CHAT_CANCEL") {
Expand Down
131 changes: 81 additions & 50 deletions LLMonFHIR/FHIR Display/InspectResourceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ struct InspecResourceView: View {
@State var error: String?
@State var interpreting = false
@State var showResourceChat = false
@State var loadingSummary = false

var resource: VersionedResource
var resource: FHIRResource

var presentAlert: Binding<Bool> {
Binding(
Expand All @@ -35,56 +36,11 @@ struct InspecResourceView: View {

var body: some View {
List {
Section("FHIR_RESOURCES_INTERPRETATION_SECTION") {
if let interpretation = fhirResourceInterpreter.interpretations[resource.id], !interpretation.isEmpty {
Text(interpretation)
.multilineTextAlignment(.leading)
if !interpreting {
Button(
action: {
showResourceChat.toggle()
},
label: {
HStack {
Image(systemName: "message.fill")
Text("FHIR_RESOURCES_INTERPRETATION_LEARN_MORE_BUTTON")
}
.frame(maxWidth: .infinity, minHeight: 40)
}
)
.buttonStyle(.borderedProminent)
}
} else {
VStack(alignment: .center) {
Text("FHIR_RESOURCES_INTERPRETATION_LOADING")
.frame(maxWidth: .infinity)
ProgressView()
.progressViewStyle(.circular)
}
}
}
Section("FHIR_RESOURCES_INTERPRETATION_RESOURCE") {
LazyText(text: resource.jsonDescription)
.fontDesign(.monospaced)
.lineLimit(1)
.font(.caption2)
}
summarySection
interpretationSection
resourceSection
}
.navigationTitle(fhirResourceSummary.summaries[resource.id]?.title ?? resource.compactDescription)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(
action: {
Task {
await interpret()
}
},
label: {
Image(systemName: "arrow.counterclockwise")
}
)
}
}
.navigationTitle(resource.displayName)
.alert("FHIR_RESOURCES_INTERPRETATION_ERROR", isPresented: presentAlert, presenting: error) { error in
Text(error)
}
Expand All @@ -101,6 +57,81 @@ struct InspecResourceView: View {
}
}

@ViewBuilder
private var summarySection: some View {
Section("FHIR_RESOURCES_SUMMARY_SECTION") {
if loadingSummary {
HStack {
Spacer()
ProgressView()
Spacer()
}
} else if let summary = fhirResourceSummary.summaries[resource.id] {
Text(summary.summary)
.multilineTextAlignment(.leading)
} else {
Button("FHIR_RESOURCES_SUMMARY_BUTTON") {
Task {
await loadSummary()
}
}
}
}
}

@ViewBuilder
private var interpretationSection: some View {
Section("FHIR_RESOURCES_INTERPRETATION_SECTION") {
if let interpretation = fhirResourceInterpreter.interpretations[resource.id], !interpretation.isEmpty {
Text(interpretation)
.multilineTextAlignment(.leading)
if !interpreting {
Button(
action: {
showResourceChat.toggle()
},
label: {
HStack {
Image(systemName: "message.fill")
Text("FHIR_RESOURCES_INTERPRETATION_LEARN_MORE_BUTTON")
}
.frame(maxWidth: .infinity, minHeight: 40)
}
)
.buttonStyle(.borderedProminent)
}
} else {
VStack(alignment: .center) {
Text("FHIR_RESOURCES_INTERPRETATION_LOADING")
.frame(maxWidth: .infinity)
ProgressView()
.progressViewStyle(.circular)
}
}
}
}

@ViewBuilder
private var resourceSection: some View {
Section("FHIR_RESOURCES_INTERPRETATION_RESOURCE") {
LazyText(text: resource.jsonDescription)
.fontDesign(.monospaced)
.lineLimit(1)
.font(.caption2)
}
}

private func loadSummary() async {
loadingSummary = true

do {
try await fhirResourceSummary.summarize(resource: resource)
} catch {
self.error = error.localizedDescription
}

loadingSummary = false
}

private func interpret() async {
interpreting = true
Expand Down
Loading

0 comments on commit 45f99cc

Please sign in to comment.