Skip to content

Commit c82867a

Browse files
committed
Merge branch 'feature/non-inverted-chat-history' into develop
2 parents 3a238ee + d763796 commit c82867a

File tree

10 files changed

+726
-445
lines changed

10 files changed

+726
-445
lines changed

Core/Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// swift-tools-version: 5.7
22
// The swift-tools-version declares the minimum version of Swift required to build this package.
33

4-
import PackageDescription
54
import Foundation
5+
import PackageDescription
66

77
// MARK: - Pro
88

@@ -242,6 +242,7 @@ let package = Package(
242242
.product(name: "Logger", package: "Tool"),
243243
.product(name: "ChatTab", package: "Tool"),
244244
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
245+
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
245246
]
246247
),
247248

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
import ChatService
2+
import ComposableArchitecture
3+
import Foundation
4+
import OpenAIService
5+
import Preferences
6+
7+
public struct ChatMessage: Equatable {
8+
public enum Role {
9+
case user
10+
case assistant
11+
case function
12+
case ignored
13+
}
14+
15+
public var id: String
16+
public var role: Role
17+
public var text: String
18+
19+
public init(id: String, role: Role, text: String) {
20+
self.id = id
21+
self.role = role
22+
self.text = text
23+
}
24+
}
25+
26+
struct Chat: ReducerProtocol {
27+
public typealias MessageID = String
28+
29+
struct State: Equatable {
30+
var title: String = "Chat"
31+
@BindingState var typedMessage = ""
32+
var history: [ChatMessage] = []
33+
@BindingState var isReceivingMessage = false
34+
var chatMenu = ChatMenu.State()
35+
}
36+
37+
enum Action: Equatable, BindableAction {
38+
case binding(BindingAction<State>)
39+
40+
case appear
41+
case sendButtonTapped
42+
case returnButtonTapped
43+
case stopRespondingButtonTapped
44+
case clearButtonTap
45+
case deleteMessageButtonTapped(MessageID)
46+
case resendMessageButtonTapped(MessageID)
47+
case setAsExtraPromptButtonTapped(MessageID)
48+
49+
case observeChatService
50+
case observeHistoryChange
51+
case observeIsReceivingMessageChange
52+
case observeSystemPromptChange
53+
case observeExtraSystemPromptChange
54+
55+
case historyChanged
56+
case isReceivingMessageChanged
57+
case systemPromptChanged
58+
case extraSystemPromptChanged
59+
60+
case chatMenu(ChatMenu.Action)
61+
}
62+
63+
let service: ChatService
64+
let id = UUID()
65+
66+
enum CancelID: Hashable {
67+
case observeHistoryChange(UUID)
68+
case observeIsReceivingMessageChange(UUID)
69+
case observeSystemPromptChange(UUID)
70+
case observeExtraSystemPromptChange(UUID)
71+
}
72+
73+
var body: some ReducerProtocol<State, Action> {
74+
BindingReducer()
75+
76+
Scope(state: \.chatMenu, action: /Action.chatMenu) {
77+
ChatMenu(service: service)
78+
}
79+
80+
Reduce { state, action in
81+
switch action {
82+
case .appear:
83+
return .run { send in
84+
await send(.observeChatService)
85+
await send(.historyChanged)
86+
await send(.isReceivingMessageChanged)
87+
await send(.systemPromptChanged)
88+
await send(.extraSystemPromptChanged)
89+
}
90+
91+
case .sendButtonTapped:
92+
guard !state.typedMessage.isEmpty else { return .none }
93+
let message = state.typedMessage
94+
state.typedMessage = ""
95+
return .run { _ in
96+
try await service.send(content: message)
97+
}
98+
99+
case .returnButtonTapped:
100+
state.typedMessage += "\n"
101+
return .none
102+
103+
case .stopRespondingButtonTapped:
104+
return .run { _ in
105+
await service.stopReceivingMessage()
106+
}
107+
108+
case .clearButtonTap:
109+
return .run { _ in
110+
await service.clearHistory()
111+
}
112+
113+
case let .deleteMessageButtonTapped(id):
114+
return .run { _ in
115+
await service.deleteMessage(id: id)
116+
}
117+
118+
case let .resendMessageButtonTapped(id):
119+
return .run { _ in
120+
try await service.resendMessage(id: id)
121+
}
122+
123+
case let .setAsExtraPromptButtonTapped(id):
124+
return .run { _ in
125+
await service.setMessageAsExtraPrompt(id: id)
126+
}
127+
128+
case .observeChatService:
129+
return .run { send in
130+
await send(.observeHistoryChange)
131+
await send(.observeIsReceivingMessageChange)
132+
await send(.observeSystemPromptChange)
133+
await send(.observeExtraSystemPromptChange)
134+
}
135+
136+
case .observeHistoryChange:
137+
return .run { send in
138+
let stream = AsyncStream<Void> { continuation in
139+
let cancellable = service.$chatHistory.sink { _ in
140+
continuation.yield()
141+
}
142+
continuation.onTermination = { _ in
143+
cancellable.cancel()
144+
}
145+
}
146+
for await _ in stream {
147+
await send(.historyChanged)
148+
}
149+
}.cancellable(id: CancelID.observeHistoryChange(id), cancelInFlight: true)
150+
151+
case .observeIsReceivingMessageChange:
152+
return .run { send in
153+
let stream = AsyncStream<Void> { continuation in
154+
let cancellable = service.$isReceivingMessage
155+
.sink { _ in
156+
continuation.yield()
157+
}
158+
continuation.onTermination = { _ in
159+
cancellable.cancel()
160+
}
161+
}
162+
for await _ in stream {
163+
await send(.isReceivingMessageChanged)
164+
}
165+
}.cancellable(
166+
id: CancelID.observeIsReceivingMessageChange(id),
167+
cancelInFlight: true
168+
)
169+
170+
case .observeSystemPromptChange:
171+
return .run { send in
172+
let stream = AsyncStream<Void> { continuation in
173+
let cancellable = service.$systemPrompt.sink { _ in
174+
continuation.yield()
175+
}
176+
continuation.onTermination = { _ in
177+
cancellable.cancel()
178+
}
179+
}
180+
for await _ in stream {
181+
await send(.systemPromptChanged)
182+
}
183+
}.cancellable(id: CancelID.observeSystemPromptChange(id), cancelInFlight: true)
184+
185+
case .observeExtraSystemPromptChange:
186+
return .run { send in
187+
let stream = AsyncStream<Void> { continuation in
188+
let cancellable = service.$extraSystemPrompt
189+
.sink { _ in
190+
continuation.yield()
191+
}
192+
continuation.onTermination = { _ in
193+
cancellable.cancel()
194+
}
195+
}
196+
for await _ in stream {
197+
await send(.extraSystemPromptChanged)
198+
}
199+
}.cancellable(id: CancelID.observeExtraSystemPromptChange(id), cancelInFlight: true)
200+
201+
case .historyChanged:
202+
state.history = service.chatHistory.map { message in
203+
.init(
204+
id: message.id,
205+
role: {
206+
switch message.role {
207+
case .system: return .ignored
208+
case .user: return .user
209+
case .assistant:
210+
if let text = message.summary ?? message.content,
211+
!text.isEmpty
212+
{
213+
return .assistant
214+
}
215+
return .ignored
216+
case .function: return .function
217+
}
218+
}(),
219+
text: message.summary ?? message.content ?? ""
220+
)
221+
}
222+
223+
state.title = {
224+
let defaultTitle = "Chat"
225+
guard let lastMessageText = state.history
226+
.filter({ $0.role == .assistant || $0.role == .user })
227+
.last?
228+
.text else { return defaultTitle }
229+
if lastMessageText.isEmpty { return defaultTitle }
230+
let trimmed = lastMessageText
231+
.trimmingCharacters(in: .punctuationCharacters)
232+
.trimmingCharacters(in: .whitespacesAndNewlines)
233+
if trimmed.starts(with: "```") {
234+
return "Code Block"
235+
} else {
236+
return trimmed
237+
}
238+
}()
239+
return .none
240+
241+
case .isReceivingMessageChanged:
242+
state.isReceivingMessage = service.isReceivingMessage
243+
return .none
244+
245+
case .systemPromptChanged:
246+
state.chatMenu.systemPrompt = service.systemPrompt
247+
return .none
248+
249+
case .extraSystemPromptChanged:
250+
state.chatMenu.extraSystemPrompt = service.extraSystemPrompt
251+
return .none
252+
253+
case .binding:
254+
return .none
255+
256+
case .chatMenu:
257+
return .none
258+
}
259+
}
260+
}
261+
}
262+
263+
struct ChatMenu: ReducerProtocol {
264+
struct State: Equatable {
265+
var systemPrompt: String = ""
266+
var extraSystemPrompt: String = ""
267+
var temperatureOverride: Double? = nil
268+
var chatModelIdOverride: String? = nil
269+
}
270+
271+
enum Action: Equatable {
272+
case appear
273+
case resetPromptButtonTapped
274+
case temperatureOverrideSelected(Double?)
275+
case chatModelIdOverrideSelected(String?)
276+
case customCommandButtonTapped(CustomCommand)
277+
}
278+
279+
let service: ChatService
280+
281+
var body: some ReducerProtocol<State, Action> {
282+
Reduce { state, action in
283+
switch action {
284+
case .appear:
285+
state.temperatureOverride = service.configuration.overriding.temperature
286+
state.chatModelIdOverride = service.configuration.overriding.modelId
287+
return .none
288+
289+
case .resetPromptButtonTapped:
290+
return .run { _ in
291+
await service.resetPrompt()
292+
}
293+
case let .temperatureOverrideSelected(temperature):
294+
state.temperatureOverride = temperature
295+
return .run { _ in
296+
service.configuration.overriding.temperature = temperature
297+
}
298+
case let .chatModelIdOverrideSelected(chatModelId):
299+
state.chatModelIdOverride = chatModelId
300+
return .run { _ in
301+
service.configuration.overriding.modelId = chatModelId
302+
}
303+
case let .customCommandButtonTapped(command):
304+
return .run { _ in
305+
try await service.handleCustomCommand(command)
306+
}
307+
}
308+
}
309+
}
310+
}
311+

0 commit comments

Comments
 (0)