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

Add Binding<Bool> and animation in ChatView for three-dots typing indicator #6

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3040959
New binding and animation for typing indicator
MatthewTurk247 Dec 19, 2023
210dbf4
Add documentation for `displayTypingIndicator` parameter
MatthewTurk247 Dec 19, 2023
df1d1b5
Encapsulated three dots in a separate view and addressed SwiftLint wa…
MatthewTurk247 Dec 21, 2023
f5b95ac
Updated header comment for `TypingIndicator`
MatthewTurk247 Dec 21, 2023
a55bc73
Added more extensive doc for `TypingIndicator`
MatthewTurk247 Dec 21, 2023
d5cc5d5
Merge pull request #1 from StanfordSpezi/main
MatthewTurk247 Jan 8, 2024
5a178a2
Bring feature branch up to speed with Philipp's changes
MatthewTurk247 Jan 8, 2024
a1d4251
Remove generic from `MessageView`
MatthewTurk247 Jan 8, 2024
f31375e
Leftover comment
MatthewTurk247 Jan 8, 2024
e622e61
Initial attempt at UI test
MatthewTurk247 Jan 17, 2024
c380368
Extend `testChat`
MatthewTurk247 Jan 18, 2024
7371b93
Neater placement of accessibility identifier
MatthewTurk247 Jan 18, 2024
95bf904
Undo accidental changes to `project.pbxproj`
MatthewTurk247 Jan 18, 2024
cabbd77
Modify comment for consistency
MatthewTurk247 Jan 18, 2024
2db787c
Merge branch 'main' into feat/chat-typing-indicator
philippzagar Jan 20, 2024
5857f35
Merge branch 'StanfordSpezi:main' into feat/chat-typing-indicator
MatthewTurk247 Jan 21, 2024
8e4a7ac
Re-build documentation
MatthewTurk247 Jan 21, 2024
67dcd21
Improvements to `TypingIndicator` preview
MatthewTurk247 Jan 21, 2024
7a2af49
Adjust circle size
MatthewTurk247 Jan 21, 2024
85ade8a
Modify `ChatView` for automatic `TypingIndicator`
MatthewTurk247 Jan 21, 2024
ea2ee1d
Renaming for typing indicator
MatthewTurk247 Jan 25, 2024
66f11c4
Revisions to `README` for updated views
MatthewTurk247 Jan 25, 2024
6581794
Add new documentation for `ChatView`
MatthewTurk247 Jan 25, 2024
c1d333e
Rename identifier to Typing Indicator
MatthewTurk247 Jan 25, 2024
ebfe647
Adjusting the DocC article
MatthewTurk247 Jan 26, 2024
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
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ These entries are mandatory for apps that utilize microphone and speech recognit

### Chat View

The [`ChatView`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatview) provides a basic reusable chat view which includes a message input field. The input can be either typed out via the iOS keyboard or provided as voice input and transcribed into written text.
The [`ChatView`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatview) provides a basic reusable chat view which includes a message input field. The input can be either typed out via the iOS keyboard or provided as voice input and transcribed into written text. It accepts an additional `messagePendingAnimation` parameter to control whether a chat bubble animation is shown for a message that is currently being composed. By default, `messagePendingAnimation` has a value of `nil` and does not show.
In addition, the [`ChatView`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatview) provides functionality to export the visualized [`Chat`](https://swiftpackageindex.com/stanfordspezi/spezichat/0.1.1/documentation/spezichat/chat) as a PDF document, JSON representation, or textual UTF-8 file (see `ChatView/ChatExportFormat`) via a Share Sheet (or Activity View).

```swift
Expand All @@ -77,6 +77,7 @@ struct ChatTestView: View {

The [`MessagesView`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/messagesview) displays a `Chat` containing multiple `ChatEntity`s with different `ChatEntity/Role`s in a typical chat-like fashion.
The `View` automatically scrolls down to the newest message that is added to the passed `Chat` SwiftUI `Binding`.
The `typingIndicator` parameter controls when a typing indicator is shown onscreen for incoming messages to `Chat`.

```swift
struct MessagesViewTestView: View {
Expand Down
8 changes: 6 additions & 2 deletions Sources/SpeziChat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public struct ChatView: View {
var disableInput: Bool
let exportFormat: ChatExportFormat?
let messagePlaceholder: String?
let messagePendingAnimation: MessagesView.TypingIndicatorDisplayMode?

@State var messageInputHeight: CGFloat = 0
@State private var showShareSheet = false
Expand All @@ -66,7 +67,7 @@ public struct ChatView: View {
public var body: some View {
ZStack {
VStack {
MessagesView($chat, bottomPadding: $messageInputHeight)
MessagesView($chat, typingIndicator: messagePendingAnimation, bottomPadding: $messageInputHeight)
.gesture(
TapGesture().onEnded {
UIApplication.shared.sendAction(
Expand Down Expand Up @@ -123,16 +124,19 @@ public struct ChatView: View {
/// - disableInput: Flag if the input view should be disabled.
/// - exportFormat: If specified, enables the export of the ``Chat`` displayed in the ``ChatView`` via a share sheet in various formats defined in ``ChatView/ChatExportFormat``.
/// - messagePlaceholder: Placeholder text that should be added in the input field.
/// - messagePendingAnimation: Parameter to control whether a chat bubble animation is shown.
public init(
_ chat: Binding<Chat>,
disableInput: Bool = false,
exportFormat: ChatExportFormat? = nil,
messagePlaceholder: String? = nil
messagePlaceholder: String? = nil,
messagePendingAnimation: MessagesView.TypingIndicatorDisplayMode? = nil
) {
self._chat = chat
self.disableInput = disableInput
self.exportFormat = exportFormat
self.messagePlaceholder = messagePlaceholder
self.messagePendingAnimation = messagePendingAnimation
}
}

Expand Down
69 changes: 69 additions & 0 deletions Sources/SpeziChat/Helpers/TypingIndicator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// 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 SwiftUI

/// Creates a typing indicator animation for pending messages.
/// The animation consists of three dots that fade in and out in a sequential, wave-like pattern.
/// It loops continuously as long as `isAnimating` is `true`.
///
/// Usage:
/// ```swift
/// struct ChatView: View {
/// var body: some View {
/// VStack {
/// MessageView(ChatEntity(role: .user, content: "User Message!"))
/// TypingIndicator()
/// }
/// }
/// }
/// ```
public struct TypingIndicator: View {
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
@State var isAnimating = false

public var body: some View {
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
HStack {
HStack(spacing: 3) {
ForEach(0..<3) { index in
Circle()
.opacity(self.isAnimating ? 1 : 0)
.foregroundStyle(.tertiary)
.animation(
Animation
.easeInOut(duration: 0.6)
.repeatForever(autoreverses: true)
.delay(0.2 * Double(index)),
value: self.isAnimating
)
.frame(width: 10)
}
.accessibilityIdentifier(String(localized: "TYPING_INDICATOR", bundle: .module))
}
.frame(width: 42, height: 12, alignment: .center)
.padding(.vertical, 4)
.onAppear {
self.isAnimating = true
}
.chatMessageStyle(alignment: .leading)
Spacer(minLength: 32)
}
}
}

#Preview {
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
ScrollView {
VStack {
MessageView(ChatEntity(role: .system, content: "System Message!"), hideMessagesWithRoles: [])
MessageView(ChatEntity(role: .system, content: "System Message (hidden)!"))
MessageView(ChatEntity(role: .function(name: "test_function"), content: "Function Message!"), hideMessagesWithRoles: [.system])
MessageView(ChatEntity(role: .user, content: "User Message!"))
TypingIndicator()
}
.padding()
}
}
31 changes: 31 additions & 0 deletions Sources/SpeziChat/MessagesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,23 @@
/// }
/// ```
public struct MessagesView: View {
/// Represents a configuration used in the initializer of ``MessagesView`` to specify when to display an animation indicating a pending message from a chat participant.
///
/// ``TypingIndicatorDisplayMode`` has two possible cases:
/// - ``TypingIndicatorDisplayMode/automatic``: The animation is shown whenever the last message in the chat is from the user,
/// and the assistant has not yet begun to respond.
/// - ``TypingIndicatorDisplayMode/manual(shouldDisplay:)``: The animation will be displayed based on the provided Boolean flag.
public enum TypingIndicatorDisplayMode {
case automatic
case manual(shouldDisplay: Bool)
}

private static let bottomSpacerIdentifier = "Bottom Spacer"

@Binding private var chat: Chat
@Binding private var bottomPadding: CGFloat
private let hideMessagesWithRoles: Set<ChatEntity.Role>
private let typingIndicator: TypingIndicatorDisplayMode?


private var keyboardPublisher: AnyPublisher<Bool, Never> {
Expand All @@ -53,6 +65,16 @@
.eraseToAnyPublisher()
}

private var shouldDisplayTypingIndicator: Bool {
MatthewTurk247 marked this conversation as resolved.
Show resolved Hide resolved
switch self.typingIndicator {
case .automatic:
self.chat.last?.role == .user
case .manual(let shouldDisplay):
shouldDisplay

Check warning on line 73 in Sources/SpeziChat/MessagesView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/MessagesView.swift#L73

Added line #L73 was not covered by tests
case .none:
false

Check warning on line 75 in Sources/SpeziChat/MessagesView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/MessagesView.swift#L75

Added line #L75 was not covered by tests
}
}

public var body: some View {
ScrollViewReader { scrollViewProxy in
Expand All @@ -61,6 +83,9 @@
ForEach(Array(chat.enumerated()), id: \.offset) { _, message in
MessageView(message, hideMessagesWithRoles: hideMessagesWithRoles)
}
if shouldDisplayTypingIndicator {
TypingIndicator()
}
Spacer()
.frame(height: bottomPadding)
.id(MessagesView.bottomSpacerIdentifier)
Expand All @@ -83,28 +108,34 @@
/// - Parameters:
/// - chat: The chat messages that should be displayed.
/// - bottomPadding: A fixed bottom padding for the messages view.
/// - typingIndicator: Indicates whether a "three dots" animation should be automatically or manually shown; default value of `nil` will result in no indicator being shown under any condition.
/// - hideMessagesWithRoles: The .system and .function roles are hidden from message view
public init(
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
_ chat: Chat,
hideMessagesWithRoles: Set<ChatEntity.Role> = MessageView.Defaults.hideMessagesWithRoles,
typingIndicator: TypingIndicatorDisplayMode? = nil,
bottomPadding: CGFloat = 0
) {
self._chat = .constant(chat)
self.hideMessagesWithRoles = hideMessagesWithRoles
self.typingIndicator = typingIndicator

Check warning on line 121 in Sources/SpeziChat/MessagesView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/MessagesView.swift#L121

Added line #L121 was not covered by tests
self._bottomPadding = .constant(bottomPadding)
}

/// - Parameters:
/// - chat: The chat messages that should be displayed.
/// - bottomPadding: A bottom padding for the messages view.
/// - typingIndicator: Indicates whether a "three dots" animation should be automatically or manually shown; default value of `nil` will result in no indicator being shown under any condition.
/// - hideMessagesWithRoles: Defines which messages should be hidden based on the passed in message roles.
public init(
_ chat: Binding<Chat>,
hideMessagesWithRoles: Set<ChatEntity.Role> = MessageView.Defaults.hideMessagesWithRoles,
typingIndicator: TypingIndicatorDisplayMode? = nil,
bottomPadding: Binding<CGFloat> = .constant(0)
) {
self._chat = chat
self.hideMessagesWithRoles = hideMessagesWithRoles
self.typingIndicator = typingIndicator
self._bottomPadding = bottomPadding
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/SpeziChat/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
}
}
}
},
"TYPING_INDICATOR" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Typing Indicator"
}
}
}
}
},
"version" : "1.0"
Expand Down
3 changes: 2 additions & 1 deletion Sources/SpeziChat/SpeziChat.docc/SpeziChat.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ These entries are mandatory for apps that utilize microphone and speech recognit

### Chat View

The ``ChatView`` provides a basic reusable chat view which includes a message input field. The input can be either typed out via the iOS keyboard or provided as voice input and transcribed into written text.
The ``ChatView`` provides a basic reusable chat view which includes a message input field. The input can be either typed out via the iOS keyboard or provided as voice input and transcribed into written text. It accepts an additional `messagePendingAnimation` parameter to control whether a chat bubble animation is shown for a message that is currently being composed. By default, `messagePendingAnimation` has a value of `nil` and does not show.
In addition, the ``ChatView`` provides functionality to export the visualized ``Chat`` as a PDF document, JSON representation, or textual UTF-8 file (see ``ChatView/ChatExportFormat``) via a Share Sheet (or Activity View).

```swift
Expand All @@ -80,6 +80,7 @@ struct ChatTestView: View {

The ``MessagesView`` displays a ``Chat`` containing multiple ``ChatEntity``s with different ``ChatEntity/Role``s in a typical chat-like fashion.
The `View` automatically scrolls down to the newest message that is added to the passed ``Chat`` SwiftUI `Binding`.
The `typingIndicator` parameter controls when a typing indicator is shown onscreen for incoming messages to `Chat`.

```swift
struct MessagesViewTestView: View {
Expand Down
4 changes: 2 additions & 2 deletions Tests/UITests/TestApp/ChatTestView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ struct ChatTestView: View {


var body: some View {
ChatView($chat, exportFormat: .pdf)
ChatView($chat, exportFormat: .pdf, messagePendingAnimation: .automatic)
.navigationTitle("SpeziChat")
.padding(.top, 16)
.onChange(of: chat) { _, newValue in
/// Append a new assistant message to the chat after sleeping for 1 second.
if newValue.last?.role == .user {
Task {
try await Task.sleep(for: .seconds(1))
try await Task.sleep(for: .seconds(5))

await MainActor.run {
chat.append(.init(role: .assistant, content: "Assistant Message Response!"))
Expand Down
4 changes: 4 additions & 0 deletions Tests/UITests/TestAppUITests/TestAppUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class TestAppUITests: XCTestCase {

sleep(1)

XCTAssert(app.otherElements["Typing Indicator"].waitForExistence(timeout: 2))

sleep(4)

XCTAssert(app.staticTexts["Assistant Message Response!"].waitForExistence(timeout: 5))
}

Expand Down
Loading