Skip to content

Commit

Permalink
AttributedString content in ChatEntity (#10)
Browse files Browse the repository at this point in the history
# `AttributedString` content in `ChatEntity`

## ♻️ Current situation & Problem
At the moment, SpeziChat is not able to store and display Markdown-based
content, which is typically returned from LLM providers like OpenAI.


## ⚙️ Release Notes 
- The content property of the `ChatEntity` can now be accessed as a
computed property that is an `AttributedString`, providing the ability
to render Markdown-formatted text in the `MessageView`.


## 📚 Documentation
In-line DocC comments written and adjusted


## ✅ Testing
--


## 📝 Code of Conduct & Contributing Guidelines 

By submitting creating this pull request, you agree to follow our [Code
of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md):
- [X] I agree to follow the [Code of
Conduct](https://github.com/StanfordSpezi/.github/blob/main/CODE_OF_CONDUCT.md)
and [Contributing
Guidelines](https://github.com/StanfordSpezi/.github/blob/main/CONTRIBUTING.md).
  • Loading branch information
philippzagar authored Feb 19, 2024
1 parent ea5e21b commit 8233230
Show file tree
Hide file tree
Showing 8 changed files with 46 additions and 19 deletions.
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

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 @@ 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 {
Expand Down Expand Up @@ -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<Content: StringProtocol>(role: Role, content: Content) {
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

0 comments on commit 8233230

Please sign in to comment.