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 Tool Interaction Views #18

Merged
merged 6 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
28 changes: 25 additions & 3 deletions Sources/SpeziChat/MessageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

private var shouldDisplayMessage: Bool {
switch chat.role {
case .user, .assistant: return true
case .user, .assistant, .assistantToolCall, .assistantToolResponse: return true
case .hidden(let type):
if case .custom(let hiddenMessageTypes) = hideMessages {
return !hiddenMessageTypes.contains(type)
Expand All @@ -55,14 +55,30 @@
}
}

private var isToolInteraction: Bool {
switch chat.role {
case .assistantToolCall, .assistantToolResponse:
return true

Check warning on line 61 in Sources/SpeziChat/MessageView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/MessageView.swift#L61

Added line #L61 was not covered by tests
LeonNissen marked this conversation as resolved.
Show resolved Hide resolved
default:
return false
}
}

public var body: some View {
if shouldDisplayMessage {
HStack {
if chat.alignment == .trailing {
Spacer(minLength: 32)
}
Text(chat.attributedContent)
.chatMessageStyle(alignment: chat.alignment)
VStack(alignment: chat.horziontalAlignment) {
if isToolInteraction {
ToolInteractionView(entity: chat)

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

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/MessageView.swift#L75

Added line #L75 was not covered by tests
} else {
Text(chat.attributedContent)
.chatMessageStyle(alignment: chat.alignment)
}
}

if chat.alignment == .leading {
Spacer(minLength: 32)
}
Expand All @@ -89,6 +105,12 @@
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!"))
MessageView(ChatEntity(role: .assistantToolCall, content: "assistent_too_call(parameter: value)"))
MessageView(ChatEntity(role: .assistantToolResponse, content: """
{
"some": "response"
}
"""))
MessageView(ChatEntity(role: .hidden(type: .unknown), content: "Hidden message! (invisible)"))
MessageView(
ChatEntity(
Expand Down
10 changes: 10 additions & 0 deletions Sources/SpeziChat/Models/ChatEntity+Alignment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//

import Foundation
import SwiftUI


extension ChatEntity {
Expand All @@ -26,4 +27,13 @@ extension ChatEntity {
return .leading
}
}

var horziontalAlignment: HorizontalAlignment {
switch self.alignment {
case .leading:
return .leading
LeonNissen marked this conversation as resolved.
Show resolved Hide resolved
case .trailing:
return .trailing
}
}
}
4 changes: 4 additions & 0 deletions Sources/SpeziChat/Models/ChatEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ public struct ChatEntity: Codable, Equatable, Hashable, Identifiable {
public enum Role: Codable, Equatable, Hashable {
case user
case assistant
case assistantToolCall
case assistantToolResponse
LeonNissen marked this conversation as resolved.
Show resolved Hide resolved
case hidden(type: ChatEntity.HiddenMessageType)


var rawValue: String {
switch self {
case .user: "user"
case .assistant: "assistant"
case .assistantToolCall: "assistant_tool_call"
case .assistantToolResponse: "assistant_tool_response"
case .hidden(let type): "hidden_\(type.name)"
}
}
Expand Down
28 changes: 28 additions & 0 deletions Sources/SpeziChat/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
}
}
}
},
"Equal sign" : {
LeonNissen marked this conversation as resolved.
Show resolved Hide resolved

},
"EXPORT_CHAT_BUTTON" : {
"localizations" : {
Expand All @@ -30,6 +33,9 @@
}
}
}
},
"Function F of X" : {

},
"MESSAGE_INPUT_TEXTFIELD" : {
"localizations" : {
Expand Down Expand Up @@ -75,6 +81,28 @@
}
}
},
"SEE_MORE" : {
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Details anzeigen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Details"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ver detalles"
}
}
}
},
"SEND_MESSAGE" : {
"localizations" : {
"de" : {
Expand Down
73 changes: 73 additions & 0 deletions Sources/SpeziChat/ToolInteractionView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// This source file is part of the Stanford Spezi open source project
//
// SPDX-FileCopyrightText: 2024 Stanford University and the project authors (see CONTRIBUTORS.md)
//
// SPDX-License-Identifier: MIT
//

import SwiftUI

/// The view that is represented when a tool call or tool reponse based on the ``ChatEntity/Role``.
struct ToolInteractionView: View {
LeonNissen marked this conversation as resolved.
Show resolved Hide resolved
let entity: ChatEntity
@State private var isExpanded = false

Check warning on line 14 in Sources/SpeziChat/ToolInteractionView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/ToolInteractionView.swift#L14

Added line #L14 was not covered by tests

var body: some View {
switch entity.role {
case .assistantToolCall:
toolCallView(content: entity.content)
case .assistantToolResponse:
toolResponseView(content: entity.content)
default:
EmptyView()
}
}

Check warning on line 25 in Sources/SpeziChat/ToolInteractionView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/ToolInteractionView.swift#L16-L25

Added lines #L16 - L25 were not covered by tests

private func toolCallView(content: String) -> some View {
HStack {
Image(systemName: "function")
.accessibilityLabel("Function F of X")
.frame(width: 20)
Text(content)
.foregroundStyle(.secondary)
.font(.footnote)
.lineLimit(isExpanded ? nil : 0)
}
.padding(.horizontal, 10)
.padding(.top, 8)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}

Check warning on line 44 in Sources/SpeziChat/ToolInteractionView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/ToolInteractionView.swift#L27-L44

Added lines #L27 - L44 were not covered by tests

private func toolResponseView(content: String) -> some View {
HStack {
Image(systemName: "equal")
.accessibilityLabel("Equal sign")
.frame(width: 20)

Group {
if content.contains(where: \.isNewline) && !isExpanded {
Text(String(localized: "SEE_MORE", bundle: .module))
.italic()
} else {
Text(content)
}
}
.foregroundStyle(.secondary)
.font(.footnote)
.lineLimit(isExpanded ? nil : 0)
}
.padding(.horizontal, 10)
.padding(.top, 8)
.padding(.bottom, 4)
.onTapGesture {
withAnimation {
isExpanded.toggle()
}
}
}

Check warning on line 72 in Sources/SpeziChat/ToolInteractionView.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziChat/ToolInteractionView.swift#L46-L72

Added lines #L46 - L72 were not covered by tests
}
4 changes: 2 additions & 2 deletions Tests/UITests/TestAppUITests/TestAppUITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
XCTAssert(app.staticTexts["SpeziChat"].waitForExistence(timeout: 1))
XCTAssert(app.staticTexts["Assistant Message!"].waitForExistence(timeout: 1))

try app.textViews["Message Input Textfield"].enter(value: "User Message!", dismissKeyboard: false)
try app.textFields["Message Input Textfield"].enter(value: "User Message!", dismissKeyboard: false)
XCTAssert(app.buttons["Send Message"].waitForExistence(timeout: 5))
app.buttons["Send Message"].tap()

Expand All @@ -48,7 +48,7 @@
throw XCTSkip("VisionOS is unstable and are skipped at the moment")
#endif

let app = XCUIApplication()

Check warning on line 51 in Tests/UITests/TestAppUITests/TestAppUITests.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS (Release, TestApp-visionOS-Release.xcresult, TestApp-visionOS-Re... / Test using xcodebuild or run fastlane

code after 'throw' will never be executed

Check warning on line 51 in Tests/UITests/TestAppUITests/TestAppUITests.swift

View workflow job for this annotation

GitHub Actions / Build and Test UI Tests visionOS (Release, TestApp-visionOS-Release.xcresult, TestApp-visionOS-Re... / Test using xcodebuild or run fastlane

code after 'throw' will never be executed
let filesApp = XCUIApplication(bundleIdentifier: "com.apple.DocumentsApp")
let maxRetries = 10

Expand All @@ -60,7 +60,7 @@

// Entering dummy chat value
XCTAssert(app.staticTexts["SpeziChat"].waitForExistence(timeout: 1))
try app.textViews["Message Input Textfield"].enter(value: "User Message!", dismissKeyboard: false)
try app.textFields["Message Input Textfield"].enter(value: "User Message!", dismissKeyboard: false)
XCTAssert(app.buttons["Send Message"].waitForExistence(timeout: 5))
app.buttons["Send Message"].tap()

Expand Down
Loading