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

AttributedString content in ChatEntity #10

Merged
merged 11 commits into from
Feb 19, 2024
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
philippzagar marked this conversation as resolved.
Show resolved Hide resolved

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 {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SpeziChat/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public struct ChatView: View {
MessageInputView($chat, messagePlaceholder: messagePlaceholder)
.disabled(disableInput)
.onPreferenceChange(MessageInputViewHeightKey.self) { newValue in
messageInputHeight = newValue
messageInputHeight = newValue + 12
}
}
}
Expand Down
5 changes: 0 additions & 5 deletions Sources/SpeziChat/MessageStyleViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -40,7 +36,6 @@ struct MessageStyleModifier: ViewModifier {

func body(content: Content) -> some View {
content
.multilineTextAlignment(multilineTextAlignment)
.padding(.horizontal, 10)
.padding(.vertical, 8)
.foregroundColor(foregroundColor)
Expand Down
6 changes: 4 additions & 2 deletions Sources/SpeziChat/MessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
}
5 changes: 3 additions & 2 deletions Sources/SpeziChat/Models/Chat.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
26 changes: 22 additions & 4 deletions Sources/SpeziChat/Models/ChatEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@


/// 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 {
Expand Down Expand Up @@ -39,13 +41,29 @@
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)
}
}

Check warning on line 56 in Sources/SpeziChat/Models/ChatEntity.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/Models/ChatEntity.swift#L54-L56

Added lines #L54 - L56 were not covered by tests


/// 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<Content: StringProtocol>(role: Role, content: Content) {
philippzagar marked this conversation as resolved.
Show resolved Hide resolved
self.role = role
self.content = content
self.content = String(content)
self.date = Date()
}
}
7 changes: 6 additions & 1 deletion Sources/SpeziChat/SpeziChat.docc/SpeziChat.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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 @@ -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!")
]


Expand All @@ -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!"))
}
}
}
Expand Down
Loading