187 lines
6.3 KiB
Swift
187 lines
6.3 KiB
Swift
//
|
|
// LLMChatInteractor.swift
|
|
// MNNLLMiOS
|
|
// Modified from ExyteChat's Chat Example
|
|
//
|
|
// Created by 游薪渝(揽清) on 2025/1/10.
|
|
//
|
|
|
|
import Combine
|
|
import ExyteChat
|
|
|
|
enum UserType {
|
|
case system
|
|
case user
|
|
case assistant
|
|
}
|
|
|
|
final class LLMChatInteractor: ChatInteractorProtocol {
|
|
var chatData: LLMChatData
|
|
var modelInfo: ModelInfo
|
|
var historyMessages: [HistoryMessage]?
|
|
var isThinkingModeEnabled: Bool = true
|
|
|
|
private let processor = ThinkResultProcessor(thinkingPrefix: "<think>", completePrefix: "</think>")
|
|
|
|
private lazy var chatState = CurrentValueSubject<[LLMChatMessage], Never>(generateStartMessages(historyMessages: historyMessages))
|
|
private lazy var sharedState = chatState.share()
|
|
|
|
private var isLoading = false
|
|
private var lastDate = Date()
|
|
|
|
private var subscriptions = Set<AnyCancellable>()
|
|
|
|
var messages: AnyPublisher<[LLMChatMessage], Never> {
|
|
sharedState.eraseToAnyPublisher()
|
|
}
|
|
|
|
var senders: [LLMChatUser] {
|
|
let members = [chatData.assistant, chatData.user]
|
|
return members
|
|
}
|
|
|
|
var otherSenders: [LLMChatUser] {
|
|
senders.filter { !$0.isCurrentUser }
|
|
}
|
|
|
|
init(modelInfo: ModelInfo, historyMessages: [HistoryMessage]? = nil) {
|
|
self.modelInfo = modelInfo
|
|
chatData = LLMChatData(modelInfo: modelInfo)
|
|
self.historyMessages = historyMessages
|
|
}
|
|
|
|
deinit {
|
|
}
|
|
|
|
/// Sends a draft message to the chat with the specified user type
|
|
/// - Parameters:
|
|
/// - draftMessage: The draft message to send
|
|
/// - userType: The type of user sending the message (user, assistant, or system)
|
|
/// - Throws: Any error that occurs during message processing
|
|
func send(draftMessage: ExyteChat.DraftMessage, userType: UserType) async throws {
|
|
if draftMessage.id != nil {
|
|
guard let index = chatState.value.firstIndex(where: { $0.uid == draftMessage.id }) else {
|
|
return
|
|
}
|
|
chatState.value.remove(at: index)
|
|
}
|
|
|
|
let status: Message.Status = .sending
|
|
|
|
var sender: LLMChatUser
|
|
switch userType {
|
|
case .user:
|
|
sender = chatData.user
|
|
case .assistant:
|
|
sender = chatData.assistant
|
|
case .system:
|
|
sender = chatData.system
|
|
}
|
|
|
|
let message: LLMChatMessage = await draftMessage.toLLMChatMessage(
|
|
id: UUID().uuidString,
|
|
user: sender,
|
|
status: status
|
|
)
|
|
|
|
await MainActor.run { [weak self] in
|
|
switch userType {
|
|
case .user, .system:
|
|
self?.chatState.value.append(message)
|
|
|
|
if userType == .user {
|
|
let assistantSender = self?.chatData.assistant ?? message.sender
|
|
let emptyMessage = LLMChatMessage(
|
|
uid: UUID().uuidString,
|
|
sender: assistantSender,
|
|
createdAt: Date(),
|
|
text: "",
|
|
images: [],
|
|
videos: [],
|
|
recording: nil,
|
|
replyMessage: nil
|
|
)
|
|
self?.chatState.value.append(emptyMessage)
|
|
}
|
|
|
|
self?.processor.startNewChat()
|
|
|
|
case .assistant:
|
|
var updateLastMsg = self?.chatState.value[(self?.chatState.value.count ?? 1) - 1]
|
|
|
|
if let tags = self?.modelInfo.tags, self?.isThinkingModeEnabled == true,
|
|
tags.contains(where: { $0.localizedCaseInsensitiveContains("Think") }) || tags.contains(where: { $0.localizedCaseInsensitiveContains("思考") }),
|
|
let text = self?.processor.process(progress: message.text)
|
|
{
|
|
updateLastMsg?.text = text
|
|
} else {
|
|
if let currentText = updateLastMsg?.text {
|
|
updateLastMsg?.text = currentText + message.text
|
|
} else {
|
|
updateLastMsg?.text = message.text
|
|
}
|
|
}
|
|
|
|
if let updatedMsg = updateLastMsg {
|
|
self?.chatState.value[(self?.chatState.value.count ?? 1) - 1] = updatedMsg
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendImage(imageURL: URL) {
|
|
Task {
|
|
await MainActor.run { [weak self] in
|
|
let image = LLMChatImage(id: UUID().uuidString, thumbnail: imageURL, full: imageURL)
|
|
|
|
let message = LLMChatMessage(uid: UUID().uuidString, sender: self?.chatData.assistant ?? LLMChatUser(uid: "0", name: ""), createdAt: Date(), text: "", images: [image], videos: [], recording: nil, replyMessage: nil)
|
|
self?.chatState.value.append(message)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Replaces the text of the last message in the chat (e.g. for in-place progress updates).
|
|
/// Uses DispatchQueue.main for immediate, synchronous-like updates without Task scheduling delay.
|
|
func updateLastMessage(text: String) {
|
|
let update = { [weak self] in
|
|
guard let self = self, !self.chatState.value.isEmpty else { return }
|
|
let lastIndex = self.chatState.value.count - 1
|
|
self.chatState.value[lastIndex].text = text
|
|
}
|
|
if Thread.isMainThread {
|
|
update()
|
|
} else {
|
|
DispatchQueue.main.async(execute: update)
|
|
}
|
|
}
|
|
|
|
func connect() {}
|
|
|
|
func disconnect() {
|
|
subscriptions.removeAll()
|
|
}
|
|
|
|
func loadNextPage() -> Future<Bool, Never> {
|
|
Future<Bool, Never> { [weak self] promise in
|
|
guard let self = self, !self.isLoading else {
|
|
promise(.success(false))
|
|
return
|
|
}
|
|
self.isLoading = true
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { [weak self] in
|
|
guard let self = self else { return }
|
|
let messages = self.generateStartMessages()
|
|
self.chatState.value = messages + self.chatState.value
|
|
self.isLoading = false
|
|
promise(.success(true))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private extension LLMChatInteractor {
|
|
func generateStartMessages(historyMessages: [HistoryMessage]? = nil) -> [LLMChatMessage] {
|
|
return chatData.greatingMessage(historyMessages: historyMessages)
|
|
}
|
|
}
|