Skip to content

Commit 25a48ea

Browse files
committed
Support global chat
1 parent fe5b6d9 commit 25a48ea

File tree

7 files changed

+95
-54
lines changed

7 files changed

+95
-54
lines changed

Core/Sources/Service/GUI/ChatProvider+Service.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import OpenAIService
44
import SuggestionWidget
55

66
extension ChatProvider {
7-
convenience init(service: ChatService, fileURL: URL, onCloseChat: @escaping () -> Void) {
7+
convenience init(
8+
service: ChatService,
9+
fileURL: URL,
10+
onCloseChat: @escaping () -> Void,
11+
onSwitchContext: @escaping () -> Void
12+
) {
813
self.init()
914
let cancellable = service.objectWillChange.sink { [weak self] in
1015
guard let self else { return }
@@ -19,7 +24,7 @@ extension ChatProvider {
1924
self.isReceivingMessage = await service.chatGPTService.isReceivingMessage
2025
}
2126
}
22-
27+
2328
service.objectWillChange.send()
2429

2530
onMessageSend = { [cancellable] message in
@@ -51,5 +56,9 @@ extension ChatProvider {
5156
onCloseChat()
5257
}
5358
}
59+
60+
self.onSwitchContext = {
61+
onSwitchContext()
62+
}
5463
}
5564
}

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ final class WidgetDataSource {
1111

1212
private init() {}
1313

14+
@discardableResult
1415
func createChatIfNeeded(for url: URL) -> ChatService {
15-
let useGlobalChat = UserDefaults.standard.value(for: \.useGlobalChat)
16+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
1617
let chat: ChatService
1718
if useGlobalChat {
1819
chat = globalChat ?? ChatService(chatGPTService: ChatGPTService())
@@ -68,30 +69,31 @@ extension WidgetDataSource: SuggestionWidgetDataSource {
6869
}
6970

7071
func chatForFile(at url: URL) async -> ChatProvider? {
71-
let useGlobalChat = UserDefaults.standard.value(for: \.useGlobalChat)
72-
if useGlobalChat, let globalChat {
73-
return .init(
74-
service: globalChat,
72+
let useGlobalChat = UserDefaults.shared.value(for: \.useGlobalChat)
73+
let buildChatProvider = { (service: ChatService) in
74+
return ChatProvider(
75+
service: service,
7576
fileURL: url,
7677
onCloseChat: { [weak self] in
77-
Task { @ServiceActor [weak self] in
78-
self?.globalChat = nil
79-
}
78+
self?.globalChat = nil
79+
},
80+
onSwitchContext: { [weak self] in
81+
UserDefaults.shared.set(!useGlobalChat, for: \.useGlobalChat)
82+
self?.createChatIfNeeded(for: url)
83+
let presenter = PresentInWindowSuggestionPresenter()
84+
presenter.presentChatRoom(fileURL: url)
8085
}
8186
)
8287
}
88+
89+
if useGlobalChat, let globalChat {
90+
return buildChatProvider(globalChat)
91+
}
8392

8493
if let service = chats[url] {
85-
return .init(
86-
service: service,
87-
fileURL: url,
88-
onCloseChat: { [weak self] in
89-
Task { @ServiceActor [weak self] in
90-
self?.chats[url] = nil
91-
}
92-
}
93-
)
94+
return buildChatProvider(service)
9495
}
96+
9597
return nil
9698
}
9799
}

Core/Sources/Service/SuggestionCommandHandler/WindowBaseCommandHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
213213
)
214214
}
215215

216-
presenter.presentChatGPTConversation(fileURL: fileURL)
216+
presenter.presentChatRoom(fileURL: fileURL)
217217
}
218218

219219
func chatWithSelection(editor: EditorContent) async throws -> UpdatedContent? {
@@ -273,6 +273,6 @@ struct WindowBaseCommandHandler: SuggestionCommandHandler {
273273
}
274274
}
275275

276-
presenter.presentChatGPTConversation(fileURL: fileURL)
276+
presenter.presentChatRoom(fileURL: fileURL)
277277
}
278278
}

Core/Sources/Service/SuggestionPresenter/PresentInWindowSuggestionPresenter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct PresentInWindowSuggestionPresenter {
4848
}
4949
}
5050

51-
func presentChatGPTConversation(fileURL: URL) {
51+
func presentChatRoom(fileURL: URL) {
5252
Task { @MainActor in
5353
let controller = GraphicalUserInterfaceController.shared.suggestionWidget
5454
controller.presentChatRoom(fileURL: fileURL)

Core/Sources/SuggestionWidget/ChatProvider.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,38 @@
11
import Foundation
2+
import SwiftUI
23

3-
public final class ChatProvider: ObservableObject, Equatable {
4+
public final class ChatProvider: ObservableObject {
45
@Published public var history: [ChatMessage] = []
56
@Published public var isReceivingMessage = false
67
public var onMessageSend: (String) -> Void
78
public var onStop: () -> Void
89
public var onClear: () -> Void
910
public var onClose: () -> Void
11+
public var onSwitchContext: () -> Void
1012

1113
public init(
1214
history: [ChatMessage] = [],
1315
isReceivingMessage: Bool = false,
1416
onMessageSend: @escaping (String) -> Void = { _ in },
1517
onStop: @escaping () -> Void = {},
1618
onClear: @escaping () -> Void = {},
17-
onClose: @escaping () -> Void = {}
19+
onClose: @escaping () -> Void = {},
20+
onSwitchContext: @escaping () -> Void = {}
1821
) {
1922
self.history = history
2023
self.isReceivingMessage = isReceivingMessage
2124
self.onMessageSend = onMessageSend
2225
self.onStop = onStop
2326
self.onClear = onClear
2427
self.onClose = onClose
28+
self.onSwitchContext = onSwitchContext
2529
}
2630

27-
public static func == (lhs: ChatProvider, rhs: ChatProvider) -> Bool {
28-
lhs.history == rhs.history && lhs.isReceivingMessage == rhs.isReceivingMessage
29-
}
30-
3131
public func send(_ message: String) { onMessageSend(message) }
3232
public func stop() { onStop() }
3333
public func clear() { onClear() }
3434
public func close() { onClose() }
35+
public func switchContext() { onSwitchContext() }
3536
}
3637

3738
public struct ChatMessage: Equatable {

Core/Sources/SuggestionWidget/SuggestionPanelContent/ChatPanel.swift

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import MarkdownUI
22
import SwiftUI
33

44
struct ChatPanel: View {
5-
@ObservedObject var viewModel: SuggestionPanelViewModel
65
@ObservedObject var chat: ChatProvider
76
@Namespace var inputAreaNamespace
87
@State var typedMessage = ""
@@ -13,8 +12,7 @@ struct ChatPanel: View {
1312
Divider()
1413
ChatPanelMessages(
1514
chat: chat,
16-
inputAreaNamespace: inputAreaNamespace,
17-
colorScheme: viewModel.colorScheme
15+
inputAreaNamespace: inputAreaNamespace
1816
)
1917
Divider()
2018
ChatPanelInputArea(
@@ -30,10 +28,18 @@ struct ChatPanel: View {
3028
}
3129

3230
struct ChatPanelToolbar: View {
33-
let chat: ChatProvider
31+
@ObservedObject var chat: ChatProvider
32+
@AppStorage(\.useGlobalChat) var useGlobalChat
3433

3534
var body: some View {
3635
HStack {
36+
Toggle(isOn: .init(get: {
37+
useGlobalChat
38+
}, set: { _ in
39+
chat.switchContext()
40+
})) { EmptyView() }
41+
.toggleStyle(GlobalChatSwitchToggleStyle())
42+
3743
Spacer()
3844

3945
Button(action: {
@@ -45,7 +51,8 @@ struct ChatPanelToolbar: View {
4551
}
4652
.buttonStyle(.plain)
4753
}
48-
.padding(.horizontal, 4)
54+
.padding(.leading, 8)
55+
.padding(.trailing, 4)
4956
.padding(.vertical, 4)
5057
.background(.regularMaterial)
5158
}
@@ -54,7 +61,7 @@ struct ChatPanelToolbar: View {
5461
struct ChatPanelMessages: View {
5562
@ObservedObject var chat: ChatProvider
5663
var inputAreaNamespace: Namespace.ID
57-
var colorScheme: ColorScheme
64+
@Environment(\.colorScheme) var colorScheme
5865
@AppStorage(\.disableLazyVStack) var disableLazyVStack
5966

6067
@ViewBuilder
@@ -74,7 +81,7 @@ struct ChatPanelMessages: View {
7481
ScrollView {
7582
vstack {
7683
let r = 6 as Double
77-
84+
7885
Spacer()
7986

8087
if chat.isReceivingMessage {
@@ -112,7 +119,6 @@ struct ChatPanelMessages: View {
112119
ForEach(chat.history.reversed(), id: \.id) { message in
113120
let text = message.text.isEmpty && !message.isUser ? "..." : message
114121
.text
115-
116122

117123
if message.isUser {
118124
Markdown(text)
@@ -310,6 +316,42 @@ struct RoundedCorners: Shape {
310316
}
311317
}
312318

319+
struct GlobalChatSwitchToggleStyle: ToggleStyle {
320+
func makeBody(configuration: Configuration) -> some View {
321+
HStack(spacing: 4) {
322+
RoundedRectangle(cornerRadius: 10, style: .circular)
323+
.foregroundColor(configuration.isOn ? Color.indigo : .gray.opacity(0.5))
324+
.frame(width: 30, height: 20, alignment: .center)
325+
.overlay(
326+
Circle()
327+
.fill(.regularMaterial)
328+
.padding(.all, 2)
329+
.overlay(
330+
Image(
331+
systemName: configuration
332+
.isOn ? "globe" : "doc.circle"
333+
)
334+
.resizable()
335+
.aspectRatio(contentMode: .fit)
336+
.frame(width: 12, height: 12, alignment: .center)
337+
.foregroundStyle(.secondary)
338+
)
339+
.offset(x: configuration.isOn ? 5 : -5, y: 0)
340+
.animation(.linear(duration: 0.1), value: configuration.isOn)
341+
342+
)
343+
.onTapGesture { configuration.isOn.toggle() }
344+
.overlay {
345+
RoundedRectangle(cornerRadius: 10, style: .circular)
346+
.stroke(.black.opacity(0.2), lineWidth: 1)
347+
}
348+
349+
Text(configuration.isOn ? "Global Chat" : "File Chat")
350+
.foregroundStyle(.tertiary)
351+
}
352+
}
353+
}
354+
313355
// MARK: - Previews
314356

315357
struct ChatPanel_Preview: PreviewProvider {
@@ -343,9 +385,7 @@ struct ChatPanel_Preview: PreviewProvider {
343385
]
344386

345387
static var previews: some View {
346-
ChatPanel(viewModel: .init(
347-
isPanelDisplayed: true
348-
), chat: .init(
388+
ChatPanel(chat: .init(
349389
history: ChatPanel_Preview.history,
350390
isReceivingMessage: true
351391
))
@@ -356,9 +396,7 @@ struct ChatPanel_Preview: PreviewProvider {
356396

357397
struct ChatPanel_EmptyChat_Preview: PreviewProvider {
358398
static var previews: some View {
359-
ChatPanel(viewModel: .init(
360-
isPanelDisplayed: true
361-
), chat: .init(
399+
ChatPanel(chat: .init(
362400
history: [],
363401
isReceivingMessage: false
364402
))
@@ -388,9 +426,7 @@ struct ChatCodeSyntaxHighlighter: CodeSyntaxHighlighter {
388426

389427
struct ChatPanel_InputText_Preview: PreviewProvider {
390428
static var previews: some View {
391-
ChatPanel(viewModel: .init(
392-
isPanelDisplayed: true
393-
), chat: .init(
429+
ChatPanel(chat: .init(
394430
history: ChatPanel_Preview.history,
395431
isReceivingMessage: false
396432
))
@@ -403,9 +439,6 @@ struct ChatPanel_InputText_Preview: PreviewProvider {
403439
struct ChatPanel_InputMultilineText_Preview: PreviewProvider {
404440
static var previews: some View {
405441
ChatPanel(
406-
viewModel: .init(
407-
isPanelDisplayed: true
408-
),
409442
chat: .init(
410443
history: ChatPanel_Preview.history,
411444
isReceivingMessage: false
@@ -420,10 +453,7 @@ struct ChatPanel_InputMultilineText_Preview: PreviewProvider {
420453

421454
struct ChatPanel_Light_Preview: PreviewProvider {
422455
static var previews: some View {
423-
ChatPanel(viewModel: .init(
424-
isPanelDisplayed: true,
425-
colorScheme: .light
426-
), chat: .init(
456+
ChatPanel(chat: .init(
427457
history: ChatPanel_Preview.history,
428458
isReceivingMessage: true
429459
))

Core/Sources/SuggestionWidget/SuggestionPanelView.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ struct SuggestionPanelView: View {
124124

125125
if let chat = viewModel.chat {
126126
if case .chat = viewModel.activeTab {
127-
ChatPanel(viewModel: viewModel, chat: chat)
127+
ChatPanel(chat: chat)
128128
.frame(maxWidth: .infinity, maxHeight: Style.panelHeight)
129129
.fixedSize(horizontal: false, vertical: true)
130130
.allowsHitTesting(viewModel.isPanelDisplayed)
@@ -146,7 +146,6 @@ struct SuggestionPanelView: View {
146146
return 1
147147
}())
148148
.animation(.easeInOut(duration: 0.2), value: viewModel.content?.contentHash)
149-
.animation(.easeInOut(duration: 0.2), value: viewModel.chat)
150149
.animation(.easeInOut(duration: 0.2), value: viewModel.activeTab)
151150
.animation(.easeInOut(duration: 0.2), value: viewModel.isPanelDisplayed)
152151
.frame(maxWidth: Style.panelWidth, maxHeight: Style.panelHeight)

0 commit comments

Comments
 (0)