Skip to content

Commit

Permalink
Update Message UI (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
PSchmiedmayer committed May 22, 2023
1 parent 7dd0d0a commit b4b770f
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 42 deletions.
39 changes: 24 additions & 15 deletions Sources/SpeziOpenAI/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,34 @@ import SwiftUI
public struct ChatView: View {
@Binding var chat: [Chat]
@Binding var disableInput: Bool
@State var messageInputHeight: CGFloat = 0


public var body: some View {
VStack {
MessagesView($chat)
.gesture(
TapGesture().onEnded {
UIApplication.shared.sendAction(
#selector(
UIResponder.resignFirstResponder
),
to: nil,
from: nil,
for: nil
)
ZStack {
VStack {
MessagesView($chat, bottomPadding: $messageInputHeight)
.gesture(
TapGesture().onEnded {
UIApplication.shared.sendAction(
#selector(
UIResponder.resignFirstResponder
),
to: nil,
from: nil,
for: nil
)
}
)
}
VStack {
Spacer()
MessageInputView($chat)
.disabled(disableInput)
.onPreferenceChange(MessageInputViewHeightKey.self) { newValue in
messageInputHeight = newValue
}
)
MessageInputView($chat)
.disabled(disableInput)
}
}
}

Expand Down
55 changes: 42 additions & 13 deletions Sources/SpeziOpenAI/MessageInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,28 @@ import SwiftUI
public struct MessageInputView: View {
@Binding var chat: [Chat]
@State var message: String = ""
@State var messageViewHeight: CGFloat = 0


public var body: some View {
HStack {
HStack(alignment: .bottom) {
TextField(
"Ask LLM on FHIR ...",
text: $message,
axis: .vertical
)
.frame(maxWidth: .infinity) // , minHeight: 32
.padding(.horizontal, 8)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(Color(.secondarySystemBackground), lineWidth: 1)
.background {
RoundedRectangle(cornerRadius: 20)
.stroke(Color(UIColor.systemGray2), lineWidth: 0.2)
.background {
RoundedRectangle(cornerRadius: 20)
.fill(.white.opacity(0.2))
}
.padding(.trailing, -30)
)
}
.lineLimit(1...5)
Button(
action: {
Expand All @@ -39,16 +44,33 @@ public struct MessageInputView: View {
},
label: {
Image(systemName: "arrow.up.circle.fill")
.font(.title2)
.padding(.horizontal, -8)
.font(.title)
.padding(.horizontal, -14)
.foregroundColor(
message.isEmpty ? Color(.systemGray6) : .accentColor
message.isEmpty ? Color(.systemGray5) : .accentColor
)
}
)
.padding(.trailing, -40)
.padding(.trailing, -38)
.padding(.bottom, 3)
}
.padding(.trailing, 23)
.padding(.horizontal, 16)
.padding(.vertical, 6)
.background(.white.opacity(0.4))
.background(.thinMaterial)
.background {
GeometryReader { proxy in
Color.clear
.onAppear {
messageViewHeight = proxy.size.height
}
.onChange(of: message) { _ in
messageViewHeight = proxy.size.height
}
}
}
.messageInputViewHeight(messageViewHeight)
}


Expand All @@ -70,9 +92,16 @@ struct MessageInputView_Previews: PreviewProvider {


static var previews: some View {
VStack {
MessagesView($chat)
MessageInputView($chat)
ZStack {
Color(.secondarySystemBackground)
.ignoresSafeArea()
VStack {
MessagesView($chat)
MessageInputView($chat)
}
.onPreferenceChange(MessageInputViewHeightKey.self) { newValue in
print("New MessageView height: \(newValue)")
}
}
}
}
24 changes: 24 additions & 0 deletions Sources/SpeziOpenAI/MessageInputViewHeightKey.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// This source file is part of the Stanford Spezi open source project
//
// SPDX-FileCopyrightText: 2022 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI


struct MessageInputViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat = 0

static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}

extension View {
func messageInputViewHeight(_ value: CGFloat) -> some View {
self.preference(key: MessageInputViewHeightKey.self, value: value)
}
}
69 changes: 62 additions & 7 deletions Sources/SpeziOpenAI/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,43 +6,99 @@
// SPDX-License-Identifier: MIT
//

import Combine
import OpenAI
import SwiftUI


/// Displays the content of a `Chat` message in a message bubble
public struct MessagesView: View {
private static let bottomSpacerIdentifier = "Bottom Spacer"


@Binding var chat: [Chat]
@Binding var bottomPadding: CGFloat
let hideSystemMessages: Bool


private var keyboardPublisher: AnyPublisher<Bool, Never> {
Publishers
.Merge(
NotificationCenter
.default
.publisher(for: UIResponder.keyboardWillShowNotification)
.map { _ in true },
NotificationCenter
.default
.publisher(for: UIResponder.keyboardWillHideNotification)
.map { _ in false }
)
.debounce(for: .seconds(0.1), scheduler: RunLoop.main)
.eraseToAnyPublisher()
}


public var body: some View {
ScrollView {
VStack {
ForEach(Array(chat.enumerated()), id: \.offset) { _, message in
MessageView(message)
ScrollViewReader { scrollViewProxy in
ScrollView {
VStack {
ForEach(Array(chat.enumerated()), id: \.offset) { _, message in
MessageView(message)
}
Spacer()
.frame(height: bottomPadding)
.id(MessagesView.bottomSpacerIdentifier)
}
.padding(.horizontal)
.onAppear {
scrollToBottom(scrollViewProxy)
}
.onChange(of: chat) { _ in
scrollToBottom(scrollViewProxy)
}
.onReceive(keyboardPublisher) { _ in
scrollToBottom(scrollViewProxy)
}
}
}
}


/// - Parameters:
/// - chat: The chat messages that should be displayed.
/// - bottomPadding: A fixed bottom padding for the messages view.
/// - hideSystemMessages: If system messages should be hidden from the chat overview.
public init(_ chat: [Chat], hideSystemMessages: Bool = true) {
public init(
_ chat: [Chat],
bottomPadding: CGFloat = 0,
hideSystemMessages: Bool = true
) {
self._chat = .constant(chat)
self._bottomPadding = .constant(bottomPadding)
self.hideSystemMessages = hideSystemMessages
}


/// - Parameters:
/// - chat: The chat messages that should be displayed.
/// - bottomPadding: A bottom padding for the messages view.
/// - hideSystemMessages: If system messages should be hidden from the chat overview.
public init(_ chat: Binding<[Chat]>, hideSystemMessages: Bool = true) {
public init(
_ chat: Binding<[Chat]>,
bottomPadding: Binding<CGFloat> = .constant(0),
hideSystemMessages: Bool = true
) {
self._chat = chat
self._bottomPadding = bottomPadding
self.hideSystemMessages = hideSystemMessages
}


private func scrollToBottom(_ scrollViewProxy: ScrollViewProxy) {
withAnimation(.easeOut) {
scrollViewProxy.scrollTo(MessagesView.bottomSpacerIdentifier)
}
}
}


Expand All @@ -56,6 +112,5 @@ struct MessagesView_Previews: PreviewProvider {
Chat(role: .assistant, content: "Assistant Message!")
]
)
.padding()
}
}
25 changes: 20 additions & 5 deletions Sources/SpeziOpenAI/OpenAIAPIKeyOnboardingStep.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,28 @@ public struct OpenAIAPIKeyOnboardingStep<ComponentStandard: Standard>: View {
OnboardingView(
titleView: {
OnboardingTitleView(
title: String(localized: "OPENAI_API_KEY_TITLE", bundle: .module),
subtitle: String(localized: "OPENAI_API_KEY_SUBTITLE", bundle: .module)
title: String(localized: "OPENAI_API_KEY_TITLE", bundle: .module)
)
},
contentView: {
TextField(String(localized: "OPENAI_API_KEY_PROMT", bundle: .module), text: apiToken)
.padding()
.textFieldStyle(.roundedBorder)
ScrollView {
VStack {
Text(String(localized: "OPENAI_API_KEY_SUBTITLE", bundle: .module))
.multilineTextAlignment(.center)
Text((try? AttributedString(
markdown: String(
localized: "OPENAI_API_KEY_SUBTITLE_HINT",
bundle: .module
)
)) ?? "")
.multilineTextAlignment(.center)
.padding(.vertical, 16)
TextField(String(localized: "OPENAI_API_KEY_PROMT", bundle: .module), text: apiToken)
.frame(height: 50)
.padding(.vertical)
.textFieldStyle(.roundedBorder)
}
}
},
actionView: {
OnboardingActionsView(
Expand All @@ -54,6 +68,7 @@ public struct OpenAIAPIKeyOnboardingStep<ComponentStandard: Standard>: View {
action()
}
)
.disabled(apiToken.wrappedValue.isEmpty)
}
)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/SpeziOpenAI/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

// MARK: OpenAIAPIKeyOnboardingStep
"OPENAI_API_KEY_TITLE" = "OpenAI API Key";
"OPENAI_API_KEY_SUBTITLE" = "Please enter your API key into the box below.";
"OPENAI_API_KEY_SUBTITLE" = "Please enter your OpenAI API key.";
"OPENAI_API_KEY_SUBTITLE_HINT" = "You can create and inspect your OpenAI API keys [in the API keys section of your OpenAI Account](https://platform.openai.com/account/api-keys).";
"OPENAI_API_KEY_PROMT" = "OpenAI API Key";
"OPENAI_API_KEY_SAVE_BUTTON" = "Next";

Expand Down
1 change: 0 additions & 1 deletion Tests/UITests/TestApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ struct ContentView: View {
var body: some View {
NavigationStack {
ChatView($chat)
.padding(.horizontal, 8)
.navigationTitle("Spezi ML")
.toolbar {
ToolbarItem {
Expand Down

0 comments on commit b4b770f

Please sign in to comment.