diff --git a/README.md b/README.md index 8fee7bb..963bfcf 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,18 @@ As [SpeziChat](https://swiftpackageindex.com/stanfordspezi/spezichat/documentati These entries are mandatory for apps that utilize microphone and speech recognition features. Failing to provide them will result in your app being unable to access these features. -## Examples +## Usage + +The underlying data model of [SpeziChat](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat) is a [`Chat`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chat). It represents the content of a typical text-based chat between user and system(s). A `Chat` is nothing more than an ordered array of [`ChatEntity`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatentity)s which contain the content of the individual messages. +A `ChatEntity` consists of a [`ChatEntity/Role`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatentity/role-swift.enum), a timestamp as well as an `String`-based content which can contain Markdown-formatted text. + +> [!NOTE] +> The [`ChatEntity`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatentity) is able to store Markdown-based content which in turn is rendered as styled text in the `ChatView`, `MessagesView`, and `MessageView`. ### 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. 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). +In addition, the [`ChatView`](https://swiftpackageindex.com/stanfordspezi/spezichat/documentation/spezichat/chatview) provides functionality to export the visualized [`Chat`](https://swiftpackageindex.com/stanfordspezi/spezichat/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 struct ChatTestView: View { diff --git a/Sources/SpeziChat/ChatView.swift b/Sources/SpeziChat/ChatView.swift index 090516b..63c730e 100644 --- a/Sources/SpeziChat/ChatView.swift +++ b/Sources/SpeziChat/ChatView.swift @@ -84,7 +84,7 @@ public struct ChatView: View { MessageInputView($chat, messagePlaceholder: messagePlaceholder) .disabled(disableInput) .onPreferenceChange(MessageInputViewHeightKey.self) { newValue in - messageInputHeight = newValue + messageInputHeight = newValue + 12 } } } diff --git a/Sources/SpeziChat/MessageStyleViewModifier.swift b/Sources/SpeziChat/MessageStyleViewModifier.swift index 3c57986..03b4105 100644 --- a/Sources/SpeziChat/MessageStyleViewModifier.swift +++ b/Sources/SpeziChat/MessageStyleViewModifier.swift @@ -21,10 +21,6 @@ struct MessageStyleModifier: ViewModifier { chatAlignment == .leading ? Color(.secondarySystemBackground) : .accentColor } - private var multilineTextAlignment: TextAlignment { - chatAlignment == .leading ? .leading : .trailing - } - private var arrowRotation: Angle { .degrees(chatAlignment == .leading ? -50 : -130) } @@ -40,7 +36,6 @@ struct MessageStyleModifier: ViewModifier { func body(content: Content) -> some View { content - .multilineTextAlignment(multilineTextAlignment) .padding(.horizontal, 10) .padding(.vertical, 8) .foregroundColor(foregroundColor) diff --git a/Sources/SpeziChat/MessageView.swift b/Sources/SpeziChat/MessageView.swift index cc17c16..e60ebe5 100644 --- a/Sources/SpeziChat/MessageView.swift +++ b/Sources/SpeziChat/MessageView.swift @@ -48,7 +48,7 @@ public struct MessageView: View { if chat.alignment == .trailing { Spacer(minLength: 32) } - Text(chat.content) + Text(chat.attributedContent) .chatMessageStyle(alignment: chat.alignment) if chat.alignment == .leading { Spacer(minLength: 32) @@ -76,7 +76,9 @@ public struct MessageView: View { MessageView(ChatEntity(role: .function(name: "test_function"), content: "Function Message!"), hideMessagesWithRoles: [.system]) MessageView(ChatEntity(role: .user, content: "User Message!")) MessageView(ChatEntity(role: .assistant, content: "Assistant Message!")) + MessageView(ChatEntity(role: .user, content: "Long User Message that spans over two lines!")) + MessageView(ChatEntity(role: .assistant, content: "Long Assistant Message that spans over two lines!")) } - .padding() + .padding() } } diff --git a/Sources/SpeziChat/Models/Chat.swift b/Sources/SpeziChat/Models/Chat.swift index 0322996..9c36898 100644 --- a/Sources/SpeziChat/Models/Chat.swift +++ b/Sources/SpeziChat/Models/Chat.swift @@ -6,6 +6,7 @@ // SPDX-License-Identifier: MIT // -/// Represents all necessary content of a typical text-based chat between the user and system(s). -/// Consists of an ordered array of ``ChatEntity``s +/// Represents the content of a typical text-based chat between user and system(s). +/// +/// A ``Chat`` is nothing more than an ordered array of ``ChatEntity``s which contain the content of the individual messages. public typealias Chat = [ChatEntity] diff --git a/Sources/SpeziChat/Models/ChatEntity.swift b/Sources/SpeziChat/Models/ChatEntity.swift index 28564c0..8a07765 100644 --- a/Sources/SpeziChat/Models/ChatEntity.swift +++ b/Sources/SpeziChat/Models/ChatEntity.swift @@ -10,7 +10,9 @@ import Foundation /// Represents the basic building block of a Spezi ``Chat``. -/// It consists of a ``ChatEntity/Role`` property as well as a `String`-based content property. +/// +/// A ``ChatEntity`` can be thought of as a single message entity within a ``Chat`` +/// It consists of a ``ChatEntity/Role``, a timestamp in the form of a `Date` as well as an `String`-based ``ChatEntity/content`` property which can contain Markdown-formatted text. public struct ChatEntity: Codable, Equatable, Hashable { /// Indicates which ``ChatEntity/Role`` is associated with a ``ChatEntity``. public enum Role: Codable, Equatable, Hashable { @@ -39,13 +41,29 @@ public struct ChatEntity: Codable, Equatable, Hashable { public let date: Date + /// Markdown-formatted ``ChatEntity/content`` as an `AttributedString`, required to render the text in Markdown-style within the ``MessageView``. + var attributedContent: AttributedString { + let markdownOptions = AttributedString.MarkdownParsingOptions( + interpretedSyntax: .inlineOnlyPreservingWhitespace, + failurePolicy: .returnPartiallyParsedIfPossible + ) + + if let attributedContent = try? AttributedString(markdown: content, options: markdownOptions) { + return attributedContent + } else { + return AttributedString(stringLiteral: content) + } + } + + /// Creates a ``ChatEntity`` which is the building block of a Spezi ``Chat``. + /// /// - Parameters: /// - role: ``ChatEntity/Role`` associated with the ``ChatEntity``. - /// - content: `String`-based content of the ``ChatEntity``. - public init(role: Role, content: String) { + /// - content: `String`-based content of the ``ChatEntity``. Can contain Markdown-formatted text. + public init(role: Role, content: Content) { self.role = role - self.content = content + self.content = String(content) self.date = Date() } } diff --git a/Sources/SpeziChat/SpeziChat.docc/SpeziChat.md b/Sources/SpeziChat/SpeziChat.docc/SpeziChat.md index c143343..f864135 100644 --- a/Sources/SpeziChat/SpeziChat.docc/SpeziChat.md +++ b/Sources/SpeziChat/SpeziChat.docc/SpeziChat.md @@ -55,7 +55,12 @@ As ``SpeziChat`` is utilizing the [SpeziSpeech](https://github.com/StanfordSpezi These entries are mandatory for apps that utilize microphone and speech recognition features. Failing to provide them will result in your app being unable to access these features. -## Examples +## Usage + +The underlying data model of ``SpeziChat`` is a ``Chat``. It represents the content of a typical text-based chat between user and system(s). A ``Chat`` is nothing more than an ordered array of ``ChatEntity``s which contain the content of the individual messages. +A ``ChatEntity`` consists of a ``ChatEntity/Role-swift.enum``, a timestamp as well as an `String`-based content which can contain Markdown-formatted text. + +> Tip: The ``ChatEntity`` is able to store Markdown-based content which in turn is rendered as styled text in the ``ChatView``, ``MessagesView``, and ``MessageView``. ### Chat View diff --git a/Tests/UITests/TestApp/ChatTestView.swift b/Tests/UITests/TestApp/ChatTestView.swift index 1a54c33..e66e41a 100644 --- a/Tests/UITests/TestApp/ChatTestView.swift +++ b/Tests/UITests/TestApp/ChatTestView.swift @@ -12,7 +12,7 @@ import SwiftUI struct ChatTestView: View { @State private var chat: Chat = [ - ChatEntity(role: .assistant, content: "Assistant Message!") + ChatEntity(role: .assistant, content: "**Assistant** Message!") ] @@ -27,7 +27,7 @@ struct ChatTestView: View { try await Task.sleep(for: .seconds(5)) await MainActor.run { - chat.append(.init(role: .assistant, content: "Assistant Message Response!")) + chat.append(.init(role: .assistant, content: "**Assistant** Message Response!")) } } }