Skip to content

Commit ed252e6

Browse files
committed
Update ChatTab to better handle tab titles
1 parent 3bf4958 commit ed252e6

3 files changed

Lines changed: 183 additions & 60 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import ChatService
2+
import Combine
3+
import Foundation
4+
import SwiftUI
5+
6+
/// A chat tab that provides a context aware chat bot, powered by ChatGPT.
7+
public class ChatGPTChatTab: ChatTab {
8+
public let service: ChatService
9+
public let provider: ChatProvider
10+
private var cancellable = Set<AnyCancellable>()
11+
12+
public func buildView() -> any View {
13+
ChatPanel(chat: provider)
14+
}
15+
16+
public init(service: ChatService = .init()) {
17+
self.service = service
18+
provider = .init(service: service)
19+
super.init(id: "Chat-" + provider.id.uuidString, title: "Chat")
20+
21+
provider.$history.sink { [weak self] _ in
22+
if let title = self?.provider.title {
23+
self?.title = title
24+
}
25+
}.store(in: &cancellable)
26+
}
27+
}
28+
29+
extension ChatProvider {
30+
convenience init(service: ChatService) {
31+
self.init(pluginIdentifiers: service.allPluginCommands)
32+
33+
let cancellable = service.objectWillChange.sink { [weak self] in
34+
guard let self else { return }
35+
Task { @MainActor in
36+
self.history = (await service.memory.history).map { message in
37+
.init(
38+
id: message.id,
39+
role: {
40+
switch message.role {
41+
case .system: return .ignored
42+
case .user: return .user
43+
case .assistant:
44+
if let text = message.summary ?? message.content, !text.isEmpty {
45+
return .assistant
46+
}
47+
return .ignored
48+
case .function: return .function
49+
}
50+
}(),
51+
text: message.summary ?? message.content ?? ""
52+
)
53+
}
54+
self.isReceivingMessage = service.isReceivingMessage
55+
self.systemPrompt = service.systemPrompt
56+
self.extraSystemPrompt = service.extraSystemPrompt
57+
}
58+
}
59+
60+
service.objectWillChange.send()
61+
62+
onMessageSend = { [cancellable] message in
63+
_ = cancellable
64+
Task {
65+
try await service.send(content: message)
66+
}
67+
}
68+
onStop = {
69+
Task {
70+
await service.stopReceivingMessage()
71+
}
72+
}
73+
74+
onClear = {
75+
Task {
76+
await service.clearHistory()
77+
}
78+
}
79+
80+
onDeleteMessage = { id in
81+
Task {
82+
await service.deleteMessage(id: id)
83+
}
84+
}
85+
86+
onResendMessage = { id in
87+
Task {
88+
try await service.resendMessage(id: id)
89+
}
90+
}
91+
92+
onResetPrompt = {
93+
Task {
94+
await service.resetPrompt()
95+
}
96+
}
97+
98+
onRunCustomCommand = { command in
99+
Task {
100+
#warning("how to handle custom command?")
101+
// let commandHandler = PseudoCommandHandler()
102+
// await commandHandler.handleCustomCommand(command)
103+
}
104+
}
105+
106+
onSetAsExtraPrompt = { id in
107+
Task {
108+
await service.setMessageAsExtraPrompt(id: id)
109+
}
110+
}
111+
}
112+
}
113+

Core/Sources/ChatTab/ChatGPT/ChatPanel.swift

Lines changed: 2 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,17 @@ import SwiftUI
66
private let r: Double = 8
77

88
public struct ChatPanel: View {
9-
let chat: ChatProvider
9+
@ObservedObject var chat: ChatProvider
1010
@Namespace var inputAreaNamespace
1111
@State var typedMessage = ""
12-
12+
1313
public init(chat: ChatProvider, typedMessage: String = "") {
1414
self.chat = chat
1515
self.typedMessage = typedMessage
1616
}
1717

1818
public var body: some View {
1919
VStack(spacing: 0) {
20-
ChatPanelToolbar(chat: chat)
21-
Divider()
2220
ChatPanelMessages(
2321
chat: chat
2422
)
@@ -32,38 +30,6 @@ public struct ChatPanel: View {
3230
}
3331
}
3432

35-
struct ChatPanelToolbar: View {
36-
@ObservedObject var chat: ChatProvider
37-
@AppStorage(\.useGlobalChat) var useGlobalChat
38-
39-
var body: some View {
40-
HStack {
41-
Button(action: {
42-
chat.close()
43-
}) {
44-
Image(systemName: "xmark")
45-
.padding(4)
46-
.foregroundStyle(.secondary)
47-
}
48-
.buttonStyle(.plain)
49-
.keyboardShortcut("w", modifiers: [.command])
50-
51-
Spacer()
52-
53-
Toggle(isOn: .init(get: {
54-
useGlobalChat
55-
}, set: { _ in
56-
chat.switchContext()
57-
})) { EmptyView() }
58-
.toggleStyle(GlobalChatSwitchToggleStyle())
59-
}
60-
.padding(.leading, 4)
61-
.padding(.trailing, 8)
62-
.padding(.vertical, 4)
63-
.background(.regularMaterial)
64-
}
65-
}
66-
6733
struct ChatPanelMessages: View {
6834
@ObservedObject var chat: ChatProvider
6935

Core/Sources/ChatTab/ChatTab.swift

Lines changed: 68 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,94 @@
1+
import ComposableArchitecture
12
import Foundation
23
import SwiftUI
34

5+
public struct ChatTabInfo: Identifiable, Equatable {
6+
public var id: String
7+
public var title: String
8+
9+
public init(id: String, title: String) {
10+
self.id = id
11+
self.title = title
12+
}
13+
}
14+
15+
public struct ChatTabInfoPreferenceKey: PreferenceKey {
16+
public static var defaultValue: [ChatTabInfo] = []
17+
public static func reduce(value: inout [ChatTabInfo], nextValue: () -> [ChatTabInfo]) {
18+
value.append(contentsOf: nextValue())
19+
}
20+
}
21+
22+
/// Every chat tab should conform to this type.
23+
public typealias ChatTab = BaseChatTab & ChatTabType
24+
425
open class BaseChatTab: Equatable {
5-
public let id: UUID
6-
7-
public static func == (lhs: BaseChatTab, rhs: BaseChatTab) -> Bool {
8-
lhs.id == rhs.id
26+
final class InfoObservable: ObservableObject {
27+
@Published var id: String
28+
@Published var title: String
29+
init(id: String, title: String) {
30+
self.title = title
31+
self.id = id
32+
}
33+
}
34+
35+
struct ContentView: View {
36+
@ObservedObject var info: InfoObservable
37+
var buildView: () -> any View
38+
var body: some View {
39+
AnyView(buildView())
40+
.preference(
41+
key: ChatTabInfoPreferenceKey.self,
42+
value: [ChatTabInfo(
43+
id: info.id,
44+
title: info.title
45+
)]
46+
)
47+
}
48+
}
49+
50+
public let id: String
51+
public var title: String {
52+
didSet { info.title = title }
953
}
10-
11-
init(id: UUID) {
54+
55+
let info: InfoObservable
56+
57+
public init(id: String, title: String) {
1258
self.id = id
59+
self.title = title
60+
info = InfoObservable(id: id, title: title)
1361
}
14-
62+
1563
@ViewBuilder
1664
public var body: some View {
1765
if let tab = self as? ChatTabType {
18-
AnyView(tab.buildView()).id(id)
66+
ContentView(info: info, buildView: tab.buildView).id(info.id)
1967
} else {
20-
EmptyView()
68+
EmptyView().id(info.id)
2169
}
2270
}
71+
72+
public static func == (lhs: BaseChatTab, rhs: BaseChatTab) -> Bool {
73+
lhs.id == rhs.id
74+
}
2375
}
2476

2577
public protocol ChatTabType {
2678
@ViewBuilder
2779
func buildView() -> any View
2880
}
2981

30-
public typealias ChatTab = BaseChatTab & ChatTabType
31-
32-
public class ChatGPTChatTab: ChatTab {
33-
public var provider: ChatProvider
34-
82+
public class EmptyChatTab: ChatTab {
3583
public func buildView() -> any View {
36-
ChatPanel(chat: provider)
37-
}
38-
39-
public init(provider: ChatProvider) {
40-
self.provider = provider
41-
super.init(id: provider.id)
84+
VStack {
85+
Text("Empty-\(id)")
86+
}
87+
.background(Color.blue)
4288
}
43-
}
4489

45-
public class EmptyChatTab: ChatTab {
46-
public func buildView() -> any View {
47-
EmptyView()
90+
public init(id: String = UUID().uuidString) {
91+
super.init(id: id, title: "Empty")
4892
}
4993
}
5094

0 commit comments

Comments
 (0)