mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-23 20:18:42 +08:00
feat(ios): translate & continue to chat & clear history (#11347)
This commit is contained in:
@@ -16,7 +16,7 @@ let package = Package(
|
||||
dependencies: [
|
||||
.package(path: "../AffineGraphQL"),
|
||||
.package(path: "../MarkdownView"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.19.0"),
|
||||
.package(url: "https://github.com/apollographql/apollo-ios.git", from: "1.18.0"),
|
||||
.package(url: "https://github.com/LaunchDarkly/swift-eventsource.git", from: "3.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-collections", from: "1.1.4"),
|
||||
.package(url: "https://github.com/Lakr233/ChidoriMenu", from: "2.4.3"),
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// UnableTo.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 4/1/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let domain = "Intelligents"
|
||||
|
||||
enum UnableTo {
|
||||
static let identifyDocumentOrWorkspace =
|
||||
NSError(
|
||||
domain: domain,
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to identify the document or workspace"]
|
||||
)
|
||||
|
||||
static let createSession = NSError(
|
||||
domain: domain,
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to create a session"]
|
||||
)
|
||||
|
||||
static let createMessage = NSError(
|
||||
domain: domain,
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to create a message"]
|
||||
)
|
||||
|
||||
static let compressImage = NSError(
|
||||
domain: domain,
|
||||
code: -1,
|
||||
userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to compress image data",
|
||||
]
|
||||
)
|
||||
|
||||
static let clearHistory = NSError(
|
||||
domain: domain,
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to clear history"]
|
||||
)
|
||||
}
|
||||
+1
-3
@@ -25,9 +25,7 @@ extension InputEditView: UIImagePickerControllerDelegate, UINavigationController
|
||||
|
||||
private func processJPEGImageData(_ image: UIImage) throws -> Data? {
|
||||
guard let data = image.jpegData(compressionQuality: 0.75) else {
|
||||
throw NSError(domain: "", code: -1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Failed to compress image data",
|
||||
])
|
||||
throw UnableTo.compressImage
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
+12
-14
@@ -8,7 +8,7 @@
|
||||
import Combine
|
||||
import UIKit
|
||||
|
||||
class InputEditView: UIView, UITextViewDelegate {
|
||||
class InputEditView: UIView {
|
||||
let mainStack = UIStackView()
|
||||
let attachmentsEditor = AttachmentBannerView()
|
||||
let textEditor = PlainTextEditView()
|
||||
@@ -22,6 +22,8 @@ class InputEditView: UIView, UITextViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
var submitAction: (() -> Void) = {}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
|
||||
@@ -38,7 +40,6 @@ class InputEditView: UIView, UITextViewDelegate {
|
||||
mainStack.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
].forEach { $0.isActive = true }
|
||||
|
||||
textEditor.delegate = self
|
||||
textEditor.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).isActive = true
|
||||
|
||||
[
|
||||
@@ -89,6 +90,15 @@ class InputEditView: UIView, UITextViewDelegate {
|
||||
.store(in: &viewModel.cancellables)
|
||||
|
||||
updateValues()
|
||||
|
||||
textEditor.textDidChange = { [weak self] text in
|
||||
self?.viewModel.text = text
|
||||
self?.updatePlaceholderVisibility()
|
||||
}
|
||||
|
||||
textEditor.textDidReturn = { [weak self] in
|
||||
self?.submitAction()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
@@ -96,18 +106,6 @@ class InputEditView: UIView, UITextViewDelegate {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
viewModel.text = textView.text
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_: UITextView) {
|
||||
updatePlaceholderVisibility()
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_: UITextView) {
|
||||
updatePlaceholderVisibility()
|
||||
}
|
||||
|
||||
func updatePlaceholderVisibility() {
|
||||
let visible = viewModel.text.isEmpty && !textEditor.isFirstResponder
|
||||
UIView.animate(withDuration: 0.25) {
|
||||
|
||||
+32
@@ -8,6 +8,9 @@
|
||||
import UIKit
|
||||
|
||||
class PlainTextEditView: UITextView, UITextViewDelegate {
|
||||
var textDidChange: ((String) -> Void) = { _ in }
|
||||
var textDidReturn: (() -> Void) = {}
|
||||
|
||||
init() {
|
||||
super.init(frame: .zero, textContainer: nil)
|
||||
|
||||
@@ -34,4 +37,33 @@ class PlainTextEditView: UITextView, UITextViewDelegate {
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
textDidChange(textView.text)
|
||||
}
|
||||
|
||||
func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
textDidChange(textView.text)
|
||||
}
|
||||
|
||||
func textViewDidEndEditing(_ textView: UITextView) {
|
||||
textDidChange(textView.text)
|
||||
}
|
||||
|
||||
func textView(_: UITextView, editMenuForTextIn _: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
.init(children: suggestedActions + [
|
||||
UIAction(title: "Insert Newline") { [weak self] _ in
|
||||
self?.insertText("\n")
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
func textView(_: UITextView, shouldChangeTextIn _: NSRange, replacementText text: String) -> Bool {
|
||||
if text == "\n" {
|
||||
textDidReturn()
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+145
-26
@@ -15,7 +15,11 @@ extension IntelligentsChatController {
|
||||
beginProgress()
|
||||
chat_createSession { session in
|
||||
self.sessionID = session ?? ""
|
||||
self.endProgress()
|
||||
self.chat_retrieveHistories {
|
||||
self.dispatchToMain {
|
||||
self.endProgress()
|
||||
}
|
||||
}
|
||||
} onFailure: { error in
|
||||
self.presentError(error) {
|
||||
if let nav = self.navigationController {
|
||||
@@ -38,6 +42,85 @@ extension IntelligentsChatController {
|
||||
self.endProgress()
|
||||
}
|
||||
}
|
||||
|
||||
func chat_clearHistory() {
|
||||
beginProgress()
|
||||
Intelligents.qlClient.perform(mutation: CleanupCopilotSessionMutation(input: .init(
|
||||
docId: metadata[.documentID] ?? "",
|
||||
sessionIds: [sessionID],
|
||||
workspaceId: metadata[.workspaceID] ?? ""
|
||||
))) { result in
|
||||
self.dispatchToMain {
|
||||
self.endProgress()
|
||||
if case let .success(value) = result,
|
||||
let sessions = value.data?.cleanupCopilotSession,
|
||||
sessions.contains(self.sessionID)
|
||||
{
|
||||
self.simpleChatContents.removeAll()
|
||||
return
|
||||
}
|
||||
self.presentError(UnableTo.clearHistory)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func chat_retrieveHistories(_ completion: @escaping () -> Void) {
|
||||
Intelligents.qlClient.fetch(query: GetCopilotHistoriesQuery(
|
||||
workspaceId: metadata[.workspaceID] ?? "",
|
||||
docId: .init(stringLiteral: metadata[.documentID] ?? ""),
|
||||
options: .some(.init(
|
||||
action: false,
|
||||
fork: false,
|
||||
limit: .init(nilLiteral: ()),
|
||||
messageOrder: .some(.case(.asc)),
|
||||
sessionId: .init(stringLiteral: sessionID),
|
||||
sessionOrder: .some(.case(.desc)),
|
||||
skip: .init(nilLiteral: ()),
|
||||
withPrompt: .init(booleanLiteral: false)
|
||||
))
|
||||
)) { [weak self] result in
|
||||
if let self,
|
||||
case let .success(value) = result,
|
||||
let object = value.data,
|
||||
let currentUser = object.__data._data["currentUser"] as? DataDict,
|
||||
let copilot = currentUser._data["copilot"] as? DataDict,
|
||||
let histories = copilot._data["histories"] as? [DataDict],
|
||||
let mostRecent = histories.first,
|
||||
let messages = mostRecent._data["messages"] as? [DataDict],
|
||||
!messages.isEmpty
|
||||
{
|
||||
print("[*] retrieved \(messages.count) messages")
|
||||
tableView.scrollToBottomOnNextUpdate = true
|
||||
tableView.alpha = 0
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) {
|
||||
self.tableView.alpha = 1
|
||||
}
|
||||
}
|
||||
for message in messages {
|
||||
guard let role = message._data["role"] as? String,
|
||||
let content = message._data["content"] as? String
|
||||
// TODO: ATTACHMENTS
|
||||
else { continue }
|
||||
switch role {
|
||||
case "assistant":
|
||||
simpleChatContents.updateValue(
|
||||
.assistant(document: content),
|
||||
forKey: UUID()
|
||||
)
|
||||
case "user":
|
||||
simpleChatContents.updateValue(
|
||||
.user(document: content),
|
||||
forKey: UUID()
|
||||
)
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension IntelligentsChatController {
|
||||
@@ -51,6 +134,7 @@ private extension IntelligentsChatController {
|
||||
|
||||
func beginProgress() {
|
||||
dispatchToMain { [self] in
|
||||
header.isUserInteractionEnabled = false
|
||||
inputBox.isUserInteractionEnabled = false
|
||||
progressView.isHidden = false
|
||||
progressView.alpha = 0
|
||||
@@ -67,6 +151,7 @@ private extension IntelligentsChatController {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
self.inputBox.editor.alpha = 1
|
||||
self.progressView.alpha = 0
|
||||
self.header.isUserInteractionEnabled = true
|
||||
} completion: { _ in
|
||||
self.inputBox.isUserInteractionEnabled = true
|
||||
self.progressView.stopAnimating()
|
||||
@@ -86,9 +171,44 @@ private extension IntelligentsChatController {
|
||||
}
|
||||
|
||||
func chat_createSession(
|
||||
forceCreateNewSession: Bool = false,
|
||||
onSuccess: @escaping (String?) -> Void,
|
||||
onFailure: @escaping (Error) -> Void
|
||||
) {
|
||||
if !forceCreateNewSession,
|
||||
let doc = metadata[.documentID],
|
||||
!doc.isEmpty
|
||||
{
|
||||
Intelligents.qlClient.fetch(query: GetCopilotSessionsQuery(
|
||||
workspaceId: .init(stringLiteral: metadata[.workspaceID] ?? ""),
|
||||
docId: .init(stringLiteral: doc),
|
||||
options: .some(QueryChatSessionsInput(InputDict([
|
||||
"action": false,
|
||||
])))
|
||||
)) { result in
|
||||
switch result {
|
||||
case let .success(value):
|
||||
if let result = value.data,
|
||||
let currentUser = result.__data._data["currentUser"] as? DataDict,
|
||||
let copilot = currentUser._data["copilot"] as? DataDict,
|
||||
let sessions = copilot._data["sessions"] as? [DataDict],
|
||||
let mostRecent = sessions.last,
|
||||
let sessionID = mostRecent._data["id"] as? String
|
||||
{
|
||||
print("[*] using existing session", sessionID)
|
||||
self.dispatchToMain { onSuccess(sessionID) }
|
||||
return
|
||||
}
|
||||
self.chat_createSession(
|
||||
forceCreateNewSession: true,
|
||||
onSuccess: onSuccess,
|
||||
onFailure: onFailure
|
||||
)
|
||||
case let .failure(error):
|
||||
self.dispatchToMain { onFailure(error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Intelligents.qlClient.perform(
|
||||
mutation: CreateCopilotSessionMutation(options: .init(
|
||||
docId: metadata[.documentID] ?? "",
|
||||
@@ -103,13 +223,7 @@ private extension IntelligentsChatController {
|
||||
self.dispatchToMain { onSuccess(session) }
|
||||
} else {
|
||||
self.dispatchToMain {
|
||||
onFailure(
|
||||
NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No session created"]
|
||||
)
|
||||
)
|
||||
onFailure(UnableTo.createSession)
|
||||
}
|
||||
}
|
||||
case let .failure(error):
|
||||
@@ -122,11 +236,15 @@ private extension IntelligentsChatController {
|
||||
let text = viewModel.text
|
||||
// let images = viewModel.attachments
|
||||
|
||||
let assistantContentID = UUID()
|
||||
dispatchToMain {
|
||||
let content = ChatContent.user(document: text)
|
||||
let key = UUID()
|
||||
self.simpleChatContents.updateValue(content, forKey: key)
|
||||
self.tableView.scrollLastCellToTop()
|
||||
self.simpleChatContents.updateValue(content, forKey: .init())
|
||||
self.simpleChatContents.updateValue(
|
||||
.assistant(document: "..."),
|
||||
forKey: assistantContentID
|
||||
)
|
||||
self.tableView.scrollToBottomOnNextUpdate = true
|
||||
}
|
||||
|
||||
let sem = DispatchSemaphore(value: 0)
|
||||
@@ -134,6 +252,12 @@ private extension IntelligentsChatController {
|
||||
Intelligents.qlClient.perform(
|
||||
mutation: CreateCopilotMessageMutation(options: .init(
|
||||
content: .init(stringLiteral: text),
|
||||
params: .some(.dictionary([
|
||||
"docs": [
|
||||
"docId": metadata[.documentID] ?? "",
|
||||
"docContent": metadata[.content] ?? "",
|
||||
],
|
||||
])),
|
||||
sessionId: sessionID
|
||||
)),
|
||||
queue: .global()
|
||||
@@ -143,13 +267,13 @@ private extension IntelligentsChatController {
|
||||
case let .success(value):
|
||||
if let messageID = value.data?.createCopilotMessage {
|
||||
print("[*] messageID", messageID)
|
||||
self.chat_processWithMessageID(sessionID: sessionID, messageID: messageID)
|
||||
self.chat_processWithMessageID(
|
||||
sessionID: sessionID,
|
||||
messageID: messageID,
|
||||
cellID: assistantContentID
|
||||
)
|
||||
} else {
|
||||
self.chat_onError(NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No message created"]
|
||||
))
|
||||
self.chat_onError(UnableTo.createMessage)
|
||||
}
|
||||
case let .failure(error):
|
||||
self.chat_onError(error)
|
||||
@@ -159,7 +283,7 @@ private extension IntelligentsChatController {
|
||||
sem.wait()
|
||||
}
|
||||
|
||||
func chat_processWithMessageID(sessionID: String, messageID: String) {
|
||||
func chat_processWithMessageID(sessionID: String, messageID: String, cellID: UUID) {
|
||||
let url = Constant.affineUpstreamURL
|
||||
.appendingPathComponent("api")
|
||||
.appendingPathComponent("copilot")
|
||||
@@ -171,19 +295,14 @@ private extension IntelligentsChatController {
|
||||
|
||||
guard let url = comps?.url else {
|
||||
assertionFailure()
|
||||
chat_onError(NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No message created"]
|
||||
))
|
||||
chat_onError(UnableTo.createMessage)
|
||||
return
|
||||
}
|
||||
|
||||
let contentIdentifier = UUID()
|
||||
dispatchToMain {
|
||||
self.simpleChatContents.updateValue(
|
||||
.assistant(document: "..."),
|
||||
forKey: contentIdentifier
|
||||
forKey: cellID
|
||||
)
|
||||
}
|
||||
|
||||
@@ -207,7 +326,7 @@ private extension IntelligentsChatController {
|
||||
self.dispatchToMain {
|
||||
document += message.data
|
||||
let content = ChatContent.assistant(document: document)
|
||||
self.simpleChatContents.updateValue(content, forKey: contentIdentifier)
|
||||
self.simpleChatContents.updateValue(content, forKey: cellID)
|
||||
}
|
||||
}
|
||||
let eventSource = EventSource(config: .init(handler: eventHandler, url: url))
|
||||
|
||||
+16
@@ -28,6 +28,22 @@ extension IntelligentsChatController {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
override var isUserInteractionEnabled: Bool {
|
||||
didSet { updateAvailabilityStyles() }
|
||||
}
|
||||
|
||||
func updateAvailabilityStyles() {
|
||||
if isUserInteractionEnabled {
|
||||
backButton.isEnabled = true
|
||||
dropMenu.isEnabled = true
|
||||
moreMenu.isEnabled = true
|
||||
} else {
|
||||
backButton.isEnabled = false
|
||||
dropMenu.isEnabled = false
|
||||
moreMenu.isEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
@objc func navigateActionBack() {
|
||||
parentViewController?.dismissInContext()
|
||||
}
|
||||
|
||||
+14
-3
@@ -30,9 +30,7 @@ public class IntelligentsChatController: UIViewController {
|
||||
didSet { updateContentToPublisher() }
|
||||
}
|
||||
|
||||
var sessionID: String = "" {
|
||||
didSet { print("[*] new sessionID: \(sessionID)") }
|
||||
}
|
||||
var sessionID: String = ""
|
||||
|
||||
public enum MetadataKey: String {
|
||||
case documentID
|
||||
@@ -84,10 +82,19 @@ public class IntelligentsChatController: UIViewController {
|
||||
view.addSubview(progressView)
|
||||
setupLayout()
|
||||
|
||||
header.moreMenu.showsMenuAsPrimaryAction = true
|
||||
header.moreMenu.menu = .init(children: [
|
||||
UIAction(title: "Clear History".localized(), image: UIImage(systemName: "eraser")) { [weak self] _ in
|
||||
self?.chat_clearHistory()
|
||||
},
|
||||
])
|
||||
|
||||
// TODO: IMPL
|
||||
header.dropMenu.isHidden = true
|
||||
inputBox.editor.controlBanner.cameraButton.isHidden = true
|
||||
inputBox.editor.controlBanner.photoButton.isHidden = true
|
||||
|
||||
updateContentToPublisher()
|
||||
chat_onLoad()
|
||||
}
|
||||
|
||||
@@ -126,6 +133,10 @@ public class IntelligentsChatController: UIViewController {
|
||||
action: #selector(chat_onSend),
|
||||
for: .touchUpInside
|
||||
)
|
||||
inputBox.editor.submitAction = { [weak self] in
|
||||
guard let self else { return }
|
||||
chat_onSend()
|
||||
}
|
||||
|
||||
progressView.hidesWhenStopped = true
|
||||
progressView.stopAnimating()
|
||||
|
||||
+21
-16
@@ -28,7 +28,7 @@ extension MessageListView {
|
||||
.eraseToAnyPublisher()
|
||||
|
||||
// after so, limit the refresh rate so we can handle them better
|
||||
let updateQueue = DispatchQueue(label: "flowdown.message-list-update-queue", qos: .userInteractive)
|
||||
let updateQueue = DispatchQueue(label: "affine.message-list-update-queue", qos: .userInteractive)
|
||||
let inQueuePublisher = publisher
|
||||
.throttle(for: .seconds(1 / 5), scheduler: updateQueue, latest: true)
|
||||
.eraseToAnyPublisher()
|
||||
@@ -49,7 +49,7 @@ extension MessageListView {
|
||||
|
||||
private func pickupElementsPair() -> (oldValue: Elements, newValue: Elements)? {
|
||||
#if DEBUG // just make sure assert is not called in release mode
|
||||
assert(!elementUpdateProcessLock.try(), "Should not call this method without lock")
|
||||
assert(!elementUpdateProcessLock.try(), "should not call this method without lock")
|
||||
#endif
|
||||
guard let distributedPendingUpdateElements else { return nil }
|
||||
|
||||
@@ -78,6 +78,11 @@ extension MessageListView {
|
||||
self.tableView.layoutIfNeeded()
|
||||
}
|
||||
tableView.contentOffset = contentOffset
|
||||
|
||||
if scrollToBottomOnNextUpdate {
|
||||
scrollToBottomOnNextUpdate = false
|
||||
scrollToBottom(useTableViewAnimation: false)
|
||||
}
|
||||
}
|
||||
|
||||
func reconfigure(enforceReload: Bool) {
|
||||
@@ -122,20 +127,20 @@ extension MessageListView {
|
||||
perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5)
|
||||
}
|
||||
|
||||
func scrollLastCellToTop(useTableViewAnimation: Bool = false) {
|
||||
guard elements.count > 1 else { return }
|
||||
guard tableView.contentSize.height > tableView.frame.height else { return }
|
||||
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) {
|
||||
self.tableView.scrollToRow(
|
||||
at: IndexPath(row: self.elements.count - 1, section: 0),
|
||||
at: .top,
|
||||
animated: useTableViewAnimation
|
||||
)
|
||||
}
|
||||
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil)
|
||||
isAutomaticScrollAnimating = true
|
||||
perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5)
|
||||
}
|
||||
// func scrollLastCellToTop(useTableViewAnimation: Bool = false) {
|
||||
// guard elements.count > 1 else { return }
|
||||
// guard tableView.contentSize.height > tableView.frame.height else { return }
|
||||
// UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.8) {
|
||||
// self.tableView.scrollToRow(
|
||||
// at: IndexPath(row: self.elements.count - 1, section: 0),
|
||||
// at: .top,
|
||||
// animated: useTableViewAnimation
|
||||
// )
|
||||
// }
|
||||
// NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(finishAutomaticScroll), object: nil)
|
||||
// isAutomaticScrollAnimating = true
|
||||
// perform(#selector(finishAutomaticScroll), with: nil, afterDelay: 0.5)
|
||||
// }
|
||||
|
||||
@objc private func finishAutomaticScroll() {
|
||||
isAutomaticScrollAnimating = false
|
||||
|
||||
+2
-1
@@ -23,8 +23,9 @@ class MessageListView: UIView {
|
||||
let elementUpdateProcessLock = NSLock()
|
||||
var distributedPendingUpdateElements: Elements? = nil
|
||||
var isAutomaticScrollAnimating: Bool = false
|
||||
var scrollToBottomOnNextUpdate = false
|
||||
|
||||
let footerView = UIView(frame: .init(x: 0, y: 0, width: 0, height: 500))
|
||||
let footerView = UIView(frame: .init(x: 0, y: 0, width: 0, height: 200))
|
||||
|
||||
init(dataPublisher: AnyPublisher<[Element], Never>) {
|
||||
super.init(frame: .zero)
|
||||
|
||||
+19
-21
@@ -15,6 +15,8 @@ extension IntelligentsEphemeralActionController {
|
||||
chatTask?.stop()
|
||||
chatTask = nil
|
||||
copilotDocumentStorage = ""
|
||||
sessionID = ""
|
||||
messageID = ""
|
||||
chat_createSession(
|
||||
documentIdentifier: documentID,
|
||||
workspaceIdentifier: workspaceID
|
||||
@@ -35,18 +37,12 @@ extension IntelligentsEphemeralActionController {
|
||||
onFailure: @escaping (Error) -> Void
|
||||
) {
|
||||
if documentIdentifier.isEmpty || workspaceIdentifier.isEmpty {
|
||||
onFailure(
|
||||
NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Unable to identify the document or workspace"]
|
||||
)
|
||||
)
|
||||
onFailure(UnableTo.identifyDocumentOrWorkspace)
|
||||
}
|
||||
Intelligents.qlClient.perform(
|
||||
mutation: CreateCopilotSessionMutation(options: .init(
|
||||
docId: documentIdentifier,
|
||||
promptName: ation.prompt.rawValue,
|
||||
promptName: action.prompt.rawValue,
|
||||
workspaceId: workspaceIdentifier
|
||||
)),
|
||||
queue: .global()
|
||||
@@ -57,13 +53,7 @@ extension IntelligentsEphemeralActionController {
|
||||
DispatchQueue.main.async { onSuccess(session) }
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
onFailure(
|
||||
NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No session created"]
|
||||
)
|
||||
)
|
||||
onFailure(UnableTo.createSession)
|
||||
}
|
||||
}
|
||||
case let .failure(error):
|
||||
@@ -73,9 +63,17 @@ extension IntelligentsEphemeralActionController {
|
||||
}
|
||||
|
||||
func beginThisRound() {
|
||||
let parms: [String: AnyHashable] = switch action {
|
||||
case let .translate(lang):
|
||||
["language": lang.rawValue]
|
||||
case .summarize:
|
||||
[:]
|
||||
}
|
||||
let json = try! CustomJSON(_jsonValue: parms)
|
||||
Intelligents.qlClient.perform(
|
||||
mutation: CreateCopilotMessageMutation(options: .init(
|
||||
content: .init(stringLiteral: "\(documentContent)"),
|
||||
params: .some(json),
|
||||
sessionId: sessionID
|
||||
)),
|
||||
queue: .global()
|
||||
@@ -83,8 +81,12 @@ extension IntelligentsEphemeralActionController {
|
||||
switch result {
|
||||
case let .success(value):
|
||||
if let messageID = value.data?.createCopilotMessage {
|
||||
print("[*] messageID", messageID)
|
||||
self.messageID = messageID
|
||||
self.chat_processWithMessageID(sessionID: self.sessionID, messageID: messageID)
|
||||
} else {
|
||||
self.presentError(UnableTo.createMessage) {
|
||||
self.close()
|
||||
}
|
||||
}
|
||||
case let .failure(error):
|
||||
self.presentError(error) {
|
||||
@@ -106,11 +108,7 @@ extension IntelligentsEphemeralActionController {
|
||||
|
||||
guard let url = comps?.url else {
|
||||
assertionFailure()
|
||||
presentError(NSError(
|
||||
domain: "Intelligents",
|
||||
code: 0,
|
||||
userInfo: [NSLocalizedDescriptionKey: "No message created"]
|
||||
))
|
||||
presentError(UnableTo.createMessage)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+23
-3
@@ -11,7 +11,7 @@ import MarkdownView
|
||||
import UIKit
|
||||
|
||||
public class IntelligentsEphemeralActionController: UIViewController {
|
||||
let ation: EphemeralAction
|
||||
let action: EphemeralAction
|
||||
let scrollView = UIScrollView()
|
||||
let stackView = UIStackView()
|
||||
|
||||
@@ -28,7 +28,13 @@ public class IntelligentsEphemeralActionController: UIViewController {
|
||||
public var documentID: String = ""
|
||||
public var workspaceID: String = ""
|
||||
public var documentContent: String = ""
|
||||
var sessionID: String = ""
|
||||
public internal(set) var sessionID: String = "" {
|
||||
didSet { print(#fileID, #function, sessionID) }
|
||||
}
|
||||
|
||||
public internal(set) var messageID: String = "" {
|
||||
didSet { print(#fileID, #function, messageID) }
|
||||
}
|
||||
|
||||
var chatTask: EventSource?
|
||||
var copilotDocumentStorage: String = "" {
|
||||
@@ -39,7 +45,7 @@ public class IntelligentsEphemeralActionController: UIViewController {
|
||||
}
|
||||
|
||||
public init(action: EphemeralAction) {
|
||||
ation = action
|
||||
self.action = action
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
title = action.title
|
||||
}
|
||||
@@ -121,6 +127,10 @@ public class IntelligentsEphemeralActionController: UIViewController {
|
||||
actionBar.retryButton.action = { [weak self] in
|
||||
self?.beginAction()
|
||||
}
|
||||
actionBar.continueToChat.action = { [weak self] in
|
||||
guard let self else { return }
|
||||
continueToChat()
|
||||
}
|
||||
}
|
||||
|
||||
func setupContentViews() {
|
||||
@@ -275,3 +285,13 @@ public class IntelligentsEphemeralActionController: UIViewController {
|
||||
) { self.scrollView.setContentOffset(bottomOffset, animated: false) }
|
||||
}
|
||||
}
|
||||
|
||||
extension IntelligentsEphemeralActionController {
|
||||
func continueToChat() {
|
||||
let chatController = IntelligentsChatController()
|
||||
chatController.metadata[.documentID] = documentID
|
||||
chatController.metadata[.workspaceID] = workspaceID
|
||||
chatController.metadata[.content] = documentContent
|
||||
navigationController?.pushViewController(chatController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user