mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-07 10:03:45 +00:00
chore: define view model (#12949)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a chat interface with message list, empty state view, and support for user, assistant, and system messages. * Added a chat manager for session and message handling, including session creation, message sending, and error management. * Implemented various chat cell types (attachments, context references, workflow status, loading, and error cells) with corresponding data models and view models. * Enabled asynchronous message sending from the input box with error alerts and automatic session creation. * Added workflow and context-related models for advanced chat features. * **Enhancements** * Improved UI responsiveness with table view updates and dynamic empty state handling. * Provided a method to clear all attachments in the input box. * **Bug Fixes / Style** * Refined code formatting, access control, and minor stylistic improvements across multiple files for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objectVersion = 56;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -92,6 +92,8 @@
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
C45499AB2D140B5000E21978 /* NBStore */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = NBStore;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -340,13 +342,9 @@
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AFFiNE/Pods-AFFiNE-frameworks.sh\"\n";
|
||||
|
||||
@@ -12,14 +12,14 @@ extension AFFiNEViewController: IntelligentsButtonDelegate {
|
||||
func onIntelligentsButtonTapped(_ button: IntelligentsButton) {
|
||||
IntelligentContext.shared.webView = webView!
|
||||
button.beginProgress()
|
||||
|
||||
IntelligentContext.shared.preparePresent() { result in
|
||||
|
||||
IntelligentContext.shared.preparePresent { result in
|
||||
button.stopProgress()
|
||||
switch result {
|
||||
case .success(let success):
|
||||
case .success:
|
||||
let controller = IntelligentsController()
|
||||
self.present(controller, animated: true)
|
||||
case .failure(let failure):
|
||||
case let .failure(failure):
|
||||
let alert = UIAlertController(
|
||||
title: "Error",
|
||||
message: failure.localizedDescription,
|
||||
|
||||
@@ -13,15 +13,15 @@ class AFFiNEViewController: CAPBridgeViewController {
|
||||
intelligentsButton.delegate = self
|
||||
dismissIntelligentsButton()
|
||||
}
|
||||
|
||||
|
||||
override func webViewConfiguration(for instanceConfiguration: InstanceConfiguration) -> WKWebViewConfiguration {
|
||||
let configuration = super.webViewConfiguration(for: instanceConfiguration)
|
||||
return configuration
|
||||
}
|
||||
|
||||
|
||||
override func webView(with frame: CGRect, configuration: WKWebViewConfiguration) -> WKWebView {
|
||||
return super.webView(with: frame, configuration: configuration)
|
||||
}
|
||||
super.webView(with: frame, configuration: configuration)
|
||||
}
|
||||
|
||||
override func capacitorDidLoad() {
|
||||
let plugins: [CAPPlugin] = [
|
||||
@@ -43,6 +43,3 @@ class AFFiNEViewController: CAPBridgeViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ final class AppConfigManager {
|
||||
struct AppConfig: Decodable {
|
||||
let affineVersion: String
|
||||
}
|
||||
|
||||
static var affineVersion: String? = nil
|
||||
|
||||
|
||||
static var affineVersion: String?
|
||||
|
||||
static func getAffineVersion() -> String {
|
||||
if affineVersion == nil {
|
||||
let file = Bundle(for: AppConfigManager.self).url(forResource: "capacitor.config", withExtension: "json")!
|
||||
@@ -14,7 +14,7 @@ final class AppConfigManager {
|
||||
let config = try! JSONDecoder().decode(AppConfig.self, from: data)
|
||||
affineVersion = config.affineVersion
|
||||
}
|
||||
|
||||
|
||||
return affineVersion!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,14 +22,14 @@ enum ApplicationBridgedWindowScript: String {
|
||||
|
||||
var requiresAsyncContext: Bool {
|
||||
switch self {
|
||||
case .getCurrentDocContentInMarkdown, .createNewDocByMarkdownInCurrentWorkspace: return true
|
||||
default: return false
|
||||
case .getCurrentDocContentInMarkdown, .createNewDocByMarkdownInCurrentWorkspace: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WKWebView {
|
||||
func evaluateScript(_ script: ApplicationBridgedWindowScript, callback: @escaping (Any?) -> ()) {
|
||||
func evaluateScript(_ script: ApplicationBridgedWindowScript, callback: @escaping (Any?) -> Void) {
|
||||
if script.requiresAsyncContext {
|
||||
callAsyncJavaScript(
|
||||
script.rawValue,
|
||||
@@ -38,7 +38,7 @@ extension WKWebView {
|
||||
in: .page
|
||||
) { result in
|
||||
switch result {
|
||||
case .success(let input):
|
||||
case let .success(input):
|
||||
callback(input)
|
||||
case .failure:
|
||||
callback(nil)
|
||||
@@ -49,5 +49,3 @@ extension WKWebView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,44 +10,44 @@ enum RequestParamError: Error {
|
||||
case request(key: String)
|
||||
}
|
||||
|
||||
extension JSValueContainer {
|
||||
public func getStringEnsure(_ key: String) throws -> String {
|
||||
guard let str = self.getString(key) else {
|
||||
public extension JSValueContainer {
|
||||
func getStringEnsure(_ key: String) throws -> String {
|
||||
guard let str = getString(key) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
public func getIntEnsure(_ key: String) throws -> Int {
|
||||
guard let int = self.getInt(key) else {
|
||||
|
||||
func getIntEnsure(_ key: String) throws -> Int {
|
||||
guard let int = getInt(key) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return int
|
||||
}
|
||||
|
||||
public func getDoubleEnsure(_ key: String) throws -> Double {
|
||||
guard let doub = self.getDouble(key) else {
|
||||
|
||||
func getDoubleEnsure(_ key: String) throws -> Double {
|
||||
guard let doub = getDouble(key) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return doub
|
||||
}
|
||||
|
||||
public func getBoolEnsure(_ key: String) throws -> Bool {
|
||||
guard let bool = self.getBool(key) else {
|
||||
|
||||
func getBoolEnsure(_ key: String) throws -> Bool {
|
||||
guard let bool = getBool(key) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return bool
|
||||
}
|
||||
|
||||
public func getArrayEnsure(_ key: String) throws -> JSArray {
|
||||
guard let arr = self.getArray(key) else {
|
||||
|
||||
func getArrayEnsure(_ key: String) throws -> JSArray {
|
||||
guard let arr = getArray(key) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
public func getArrayEnsure<T>(_ key: String, _ ofType: T.Type) throws -> [T] {
|
||||
guard let arr = self.getArray(key, ofType) else {
|
||||
|
||||
func getArrayEnsure<T>(_ key: String, _ ofType: T.Type) throws -> [T] {
|
||||
guard let arr = getArray(key, ofType) else {
|
||||
throw RequestParamError.request(key: key)
|
||||
}
|
||||
return arr
|
||||
|
||||
@@ -8,15 +8,15 @@
|
||||
import Foundation
|
||||
|
||||
final class Mutex<Wrapped>: @unchecked Sendable {
|
||||
private let lock = NSLock.init()
|
||||
private let lock = NSLock()
|
||||
private var wrapped: Wrapped
|
||||
|
||||
|
||||
init(_ wrapped: Wrapped) {
|
||||
self.wrapped = wrapped
|
||||
}
|
||||
|
||||
|
||||
func withLock<R>(_ body: @Sendable (inout Wrapped) throws -> R) rethrows -> R {
|
||||
self.lock.lock()
|
||||
lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
return try body(&wrapped)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "signInPassword", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "signOut", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
|
||||
@objc public func signInMagicLink(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -18,7 +18,7 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let email = try call.getStringEnsure("email")
|
||||
let token = try call.getStringEnsure("token")
|
||||
let clientNonce = call.getString("clientNonce")
|
||||
|
||||
|
||||
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/magic-link", headers: [:], body: ["email": email, "token": token, "client_nonce": clientNonce])
|
||||
|
||||
if response.statusCode >= 400 {
|
||||
@@ -28,19 +28,19 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
call.reject("Failed to sign in")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||
call.reject("token not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
call.resolve(["token": token])
|
||||
} catch {
|
||||
call.reject("Failed to sign in, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public func signInOauth(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -48,9 +48,9 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let code = try call.getStringEnsure("code")
|
||||
let state = try call.getStringEnsure("state")
|
||||
let clientNonce = call.getString("clientNonce")
|
||||
|
||||
|
||||
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/oauth/callback", headers: [:], body: ["code": code, "state": state, "client_nonce": clientNonce])
|
||||
|
||||
|
||||
if response.statusCode >= 400 {
|
||||
if let textBody = String(data: data, encoding: .utf8) {
|
||||
call.reject(textBody)
|
||||
@@ -58,19 +58,19 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
call.reject("Failed to sign in")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||
call.reject("token not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
call.resolve(["token": token])
|
||||
} catch {
|
||||
call.reject("Failed to sign in, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public func signInPassword(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -79,12 +79,12 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let password = try call.getStringEnsure("password")
|
||||
let verifyToken = call.getString("verifyToken")
|
||||
let challenge = call.getString("challenge")
|
||||
|
||||
|
||||
let (data, response) = try await self.fetch(endpoint, method: "POST", action: "/api/auth/sign-in", headers: [
|
||||
"x-captcha-token": verifyToken,
|
||||
"x-captcha-challenge": challenge
|
||||
"x-captcha-challenge": challenge,
|
||||
], body: ["email": email, "password": password])
|
||||
|
||||
|
||||
if response.statusCode >= 400 {
|
||||
if let textBody = String(data: data, encoding: .utf8) {
|
||||
call.reject(textBody)
|
||||
@@ -92,24 +92,24 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
call.reject("Failed to sign in")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
guard let token = try self.tokenFromCookie(endpoint) else {
|
||||
call.reject("token not found")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
call.resolve(["token": token])
|
||||
} catch {
|
||||
call.reject("Failed to sign in, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc public func signOut(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let endpoint = try call.getStringEnsure("endpoint")
|
||||
|
||||
|
||||
let (data, response) = try await self.fetch(endpoint, method: "GET", action: "/api/auth/sign-out", headers: [:], body: nil)
|
||||
|
||||
if response.statusCode >= 400 {
|
||||
@@ -119,20 +119,19 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
call.reject("Failed to sign in")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
call.resolve(["ok": true])
|
||||
} catch {
|
||||
call.reject("Failed to sign in, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func tokenFromCookie(_ endpoint: String) throws -> String? {
|
||||
guard let endpointUrl = URL(string: endpoint) else {
|
||||
throw AuthError.invalidEndpoint
|
||||
}
|
||||
|
||||
|
||||
if let cookie = HTTPCookieStorage.shared.cookies(for: endpointUrl)?.first(where: {
|
||||
$0.name == "affine_session"
|
||||
}) {
|
||||
@@ -141,14 +140,14 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func fetch(_ endpoint: String, method: String, action: String, headers: Dictionary<String, String?>, body: Encodable?) async throws -> (Data, HTTPURLResponse) {
|
||||
|
||||
private func fetch(_ endpoint: String, method: String, action: String, headers: [String: String?], body: Encodable?) async throws -> (Data, HTTPURLResponse) {
|
||||
guard let targetUrl = URL(string: "\(endpoint)\(action)") else {
|
||||
throw AuthError.invalidEndpoint
|
||||
}
|
||||
|
||||
var request = URLRequest(url: targetUrl);
|
||||
request.httpMethod = method;
|
||||
|
||||
var request = URLRequest(url: targetUrl)
|
||||
request.httpMethod = method
|
||||
request.httpShouldHandleCookies = true
|
||||
for (key, value) in headers {
|
||||
request.setValue(value, forHTTPHeaderField: key)
|
||||
@@ -159,8 +158,8 @@ public class AuthPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
request.setValue(AppConfigManager.getAffineVersion(), forHTTPHeaderField: "x-affine-version")
|
||||
request.timeoutInterval = 10 // time out 10s
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request);
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw AuthError.internalError
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Capacitor
|
||||
import Foundation
|
||||
|
||||
//@objc(IntelligentsPlugin)
|
||||
//public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
// @objc(IntelligentsPlugin)
|
||||
// public class IntelligentsPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
// public let identifier = "IntelligentsPlugin"
|
||||
// public let jsName = "Intelligents"
|
||||
// public let pluginMethods: [CAPPluginMethod] = [
|
||||
@@ -33,4 +33,4 @@ import Foundation
|
||||
// call.resolve()
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
// }
|
||||
|
||||
@@ -4,7 +4,7 @@ import Foundation
|
||||
@objc(NbStorePlugin)
|
||||
public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
private let docStoragePool: DocStoragePool = newDocStoragePool()
|
||||
|
||||
|
||||
public let identifier = "NbStorePlugin"
|
||||
public let jsName = "NbStoreDocStorage"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
@@ -37,7 +37,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
CAPPluginMethod(name: "getBlobUploadedAt", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "setBlobUploadedAt", returnType: CAPPluginReturnPromise),
|
||||
]
|
||||
|
||||
|
||||
@objc func connect(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -52,10 +52,10 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let peerDir = documentDir.appending(path: "workspaces")
|
||||
.appending(path: spaceType)
|
||||
.appending(path:
|
||||
peer
|
||||
.replacing(#/[\/!@#$%^&*()+~`"':;,?<>|]/#, with: "_")
|
||||
.replacing(/_+/, with: "_")
|
||||
.replacing(/_+$/, with: ""))
|
||||
peer
|
||||
.replacing(#/[\/!@#$%^&*()+~`"':;,?<>|]/#, with: "_")
|
||||
.replacing(/_+/, with: "_")
|
||||
.replacing(/_+$/, with: ""))
|
||||
try FileManager.default.createDirectory(atPath: peerDir.path(), withIntermediateDirectories: true)
|
||||
let db = peerDir.appending(path: spaceId + ".db")
|
||||
try await docStoragePool.connect(universalId: id, path: db.path())
|
||||
@@ -65,7 +65,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func disconnect(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -77,7 +77,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setSpaceId(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -90,7 +90,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func pushUpdate(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -104,13 +104,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getDocSnapshot(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
|
||||
|
||||
if let record = try await docStoragePool.getDocSnapshot(universalId: id, docId: docId) {
|
||||
call.resolve([
|
||||
"docId": record.docId,
|
||||
@@ -125,7 +125,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setDocSnapshot(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -143,7 +143,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getDocUpdates(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -161,14 +161,14 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func markUpdatesMerged(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
let times = try call.getArrayEnsure("timestamps", Int64.self)
|
||||
|
||||
|
||||
let count = try await docStoragePool.markUpdatesMerged(universalId: id, docId: docId, updates: times)
|
||||
call.resolve(["count": count])
|
||||
} catch {
|
||||
@@ -176,13 +176,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func deleteDoc(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
|
||||
|
||||
try await docStoragePool.deleteDoc(universalId: id, docId: docId)
|
||||
call.resolve()
|
||||
} catch {
|
||||
@@ -190,13 +190,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getDocClocks(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let after = call.getInt("after")
|
||||
|
||||
|
||||
let docClocks = try await docStoragePool.getDocClocks(
|
||||
universalId: id,
|
||||
after: after != nil ? Int64(after!) : nil
|
||||
@@ -211,7 +211,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getDocClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -230,7 +230,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getBlob(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -242,7 +242,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
"data": blob.data,
|
||||
"mime": blob.mime,
|
||||
"size": blob.size,
|
||||
"createdAt": blob.createdAt
|
||||
"createdAt": blob.createdAt,
|
||||
])
|
||||
} else {
|
||||
call.resolve()
|
||||
@@ -252,7 +252,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setBlob(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -267,7 +267,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func deleteBlob(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -281,7 +281,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func releaseBlobs(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -293,7 +293,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func listBlobs(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -311,13 +311,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerRemoteClocks(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
|
||||
|
||||
let clocks = try await docStoragePool.getPeerRemoteClocks(universalId: id, peer: peer)
|
||||
let mapped = clocks.map { [
|
||||
"docId": $0.docId,
|
||||
@@ -329,14 +329,14 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerRemoteClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
|
||||
|
||||
if let clock = try await docStoragePool.getPeerRemoteClock(universalId: id, peer: peer, docId: docId) {
|
||||
call.resolve([
|
||||
"docId": clock.docId,
|
||||
@@ -345,13 +345,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
} else {
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
call.reject("Failed to get peer remote clock, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setPeerRemoteClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -371,13 +371,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerPulledRemoteClocks(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
|
||||
|
||||
let clocks = try await docStoragePool.getPeerPulledRemoteClocks(universalId: id, peer: peer)
|
||||
let mapped = clocks.map { [
|
||||
"docId": $0.docId,
|
||||
@@ -389,14 +389,14 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerPulledRemoteClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
|
||||
|
||||
if let clock = try await docStoragePool.getPeerPulledRemoteClock(universalId: id, peer: peer, docId: docId) {
|
||||
call.resolve([
|
||||
"docId": clock.docId,
|
||||
@@ -405,13 +405,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
} else {
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
|
||||
} catch {
|
||||
call.reject("Failed to get peer pulled remote clock, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setPeerPulledRemoteClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -419,7 +419,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
let timestamp = try call.getIntEnsure("timestamp")
|
||||
|
||||
|
||||
try await docStoragePool.setPeerPulledRemoteClock(
|
||||
universalId: id,
|
||||
peer: peer,
|
||||
@@ -432,7 +432,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerPushedClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -452,7 +452,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getPeerPushedClocks(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -464,13 +464,13 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
"timestamp": $0.timestamp,
|
||||
] }
|
||||
call.resolve(["clocks": mapped])
|
||||
|
||||
|
||||
} catch {
|
||||
call.reject("Failed to get peer pushed clocks, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setPeerPushedClock(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -478,7 +478,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let docId = try call.getStringEnsure("docId")
|
||||
let timestamp = try call.getIntEnsure("timestamp")
|
||||
|
||||
|
||||
try await docStoragePool.setPeerPushedClock(
|
||||
universalId: id,
|
||||
peer: peer,
|
||||
@@ -491,29 +491,29 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func getBlobUploadedAt(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
let id = try call.getStringEnsure("id")
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let blobId = try call.getStringEnsure("blobId")
|
||||
|
||||
|
||||
let uploadedAt = try await docStoragePool.getBlobUploadedAt(
|
||||
universalId: id,
|
||||
peer: peer,
|
||||
blobId: blobId
|
||||
)
|
||||
|
||||
|
||||
call.resolve([
|
||||
"uploadedAt": uploadedAt as Any
|
||||
"uploadedAt": uploadedAt as Any,
|
||||
])
|
||||
} catch {
|
||||
call.reject("Failed to get blob uploaded, \(error)", nil, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func setBlobUploadedAt(_ call: CAPPluginCall) {
|
||||
Task {
|
||||
do {
|
||||
@@ -521,7 +521,7 @@ public class NbStorePlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
let peer = try call.getStringEnsure("peer")
|
||||
let blobId = try call.getStringEnsure("blobId")
|
||||
let uploadedAt = call.getInt("uploadedAt")
|
||||
|
||||
|
||||
try await docStoragePool.setBlobUploadedAt(
|
||||
universalId: id,
|
||||
peer: peer,
|
||||
|
||||
@@ -11,26 +11,24 @@ class SafeWKURLSchemeTask: WKURLSchemeTask, NSObject {
|
||||
var origin: any WKURLSchemeTask
|
||||
init(origin: any WKURLSchemeTask) {
|
||||
self.origin = origin
|
||||
self.request = origin.request
|
||||
request = origin.request
|
||||
}
|
||||
|
||||
|
||||
var request: URLRequest
|
||||
|
||||
func didReceive(_ response: URLResponse) {
|
||||
|
||||
func didReceive(_: URLResponse) {
|
||||
<#code#>
|
||||
}
|
||||
|
||||
func didReceive(_ data: Data) {
|
||||
self.origin.didReceive(<#T##response: URLResponse##URLResponse#>)
|
||||
|
||||
func didReceive(_: Data) {
|
||||
origin.didReceive(<#T##response: URLResponse##URLResponse#>)
|
||||
}
|
||||
|
||||
|
||||
func didFinish() {
|
||||
self.origin.didFinish()
|
||||
origin.didFinish()
|
||||
}
|
||||
|
||||
|
||||
func didFailWithError(_ error: any Error) {
|
||||
self.origin.didFailWithError(error)
|
||||
origin.didFailWithError(error)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -39,4 +39,3 @@ public enum CustomJSON: CustomScalarType, Hashable {
|
||||
hasher.combine(_jsonValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// ChatManager+ContextModels.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Foundation
|
||||
|
||||
// MARK: - ChatManager Context Models Extension
|
||||
|
||||
extension ChatManager {
|
||||
// MARK: - Context Models
|
||||
|
||||
struct ContextReference: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var fileId: String?
|
||||
var docId: String?
|
||||
var chunk: Int
|
||||
var content: String
|
||||
var distance: Double
|
||||
var highlightedContent: String?
|
||||
|
||||
init(fileId: String? = nil, docId: String? = nil, chunk: Int, content: String, distance: Double, highlightedContent: String? = nil) {
|
||||
id = UUID().uuidString
|
||||
self.fileId = fileId
|
||||
self.docId = docId
|
||||
self.chunk = chunk
|
||||
self.content = content
|
||||
self.distance = distance
|
||||
self.highlightedContent = highlightedContent
|
||||
}
|
||||
}
|
||||
|
||||
struct CopilotContext: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var sessionId: String
|
||||
var workspaceId: String
|
||||
var files: [ContextFile]
|
||||
var docs: [ContextDoc]
|
||||
var categories: [ContextCategory]
|
||||
|
||||
init(id: String, sessionId: String, workspaceId: String, files: [ContextFile] = [], docs: [ContextDoc] = [], categories: [ContextCategory] = []) {
|
||||
self.id = id
|
||||
self.sessionId = sessionId
|
||||
self.workspaceId = workspaceId
|
||||
self.files = files
|
||||
self.docs = docs
|
||||
self.categories = categories
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextFile: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var contextId: String
|
||||
var blobId: String
|
||||
var fileName: String?
|
||||
var fileSize: Int?
|
||||
var mimeType: String?
|
||||
var embeddingStatus: ContextEmbedStatus?
|
||||
var createdAt: DateTime?
|
||||
|
||||
var createdDate: Date? {
|
||||
createdAt?.decoded
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextDoc: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var contextId: String
|
||||
var docId: String
|
||||
var title: String?
|
||||
var embeddingStatus: ContextEmbedStatus?
|
||||
var createdAt: DateTime?
|
||||
|
||||
var createdDate: Date? {
|
||||
createdAt?.decoded
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextCategory: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var contextId: String
|
||||
var type: ContextCategoryType
|
||||
var docs: [String]
|
||||
var name: String?
|
||||
var createdAt: DateTime?
|
||||
|
||||
var createdDate: Date? {
|
||||
createdAt?.decoded
|
||||
}
|
||||
}
|
||||
|
||||
enum ContextEmbedStatus: String, Codable, CaseIterable {
|
||||
case pending = "Pending"
|
||||
case failed = "Failed"
|
||||
case completed = "Completed"
|
||||
}
|
||||
|
||||
enum ContextCategoryType: String, Codable, CaseIterable {
|
||||
case tag = "TAG"
|
||||
case collection = "COLLECTION"
|
||||
}
|
||||
|
||||
struct MatchContextResult: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var fileId: String?
|
||||
var docId: String?
|
||||
var chunk: Int
|
||||
var content: String
|
||||
var distance: Double
|
||||
var highlightedContent: String?
|
||||
|
||||
init(fileId: String? = nil, docId: String? = nil, chunk: Int, content: String, distance: Double, highlightedContent: String? = nil) {
|
||||
id = UUID().uuidString
|
||||
self.fileId = fileId
|
||||
self.docId = docId
|
||||
self.chunk = chunk
|
||||
self.content = content
|
||||
self.distance = distance
|
||||
self.highlightedContent = highlightedContent
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// ChatManager+InputModels.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - ChatManager Input Models Extension
|
||||
|
||||
extension ChatManager {
|
||||
// MARK: - Input Models
|
||||
|
||||
struct AddContextFileInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var blobId: String
|
||||
}
|
||||
|
||||
struct RemoveContextFileInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var fileId: String
|
||||
}
|
||||
|
||||
struct AddContextDocInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var docId: String
|
||||
}
|
||||
|
||||
struct RemoveContextDocInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var docId: String
|
||||
}
|
||||
|
||||
struct AddContextCategoryInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var docs: [String]
|
||||
}
|
||||
|
||||
struct RemoveContextCategoryInput: Codable, Equatable, Hashable {
|
||||
var contextId: String
|
||||
var categoryId: String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ChatManager+WorkflowModels.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Foundation
|
||||
|
||||
// MARK: - ChatManager Workflow Models Extension
|
||||
|
||||
extension ChatManager {
|
||||
// MARK: - Workflow Models
|
||||
|
||||
struct WorkflowEventData: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var status: String
|
||||
var type: String
|
||||
var progress: Double?
|
||||
var message: String?
|
||||
|
||||
init(status: String, type: String, progress: Double? = nil, message: String? = nil) {
|
||||
id = UUID().uuidString
|
||||
self.status = status
|
||||
self.type = type
|
||||
self.progress = progress
|
||||
self.message = message
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceEmbeddingStatus: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var workspaceId: String
|
||||
var total: Int
|
||||
var embedded: Int
|
||||
|
||||
var progress: Double {
|
||||
total > 0 ? Double(embedded) / Double(total) : 0.0
|
||||
}
|
||||
|
||||
init(workspaceId: String, total: Int, embedded: Int) {
|
||||
id = workspaceId
|
||||
self.workspaceId = workspaceId
|
||||
self.total = total
|
||||
self.embedded = embedded
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatEvent: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var type: ChatEventType
|
||||
var data: String
|
||||
var timestamp: DateTime?
|
||||
|
||||
var timestampDate: Date? {
|
||||
timestamp?.decoded
|
||||
}
|
||||
|
||||
init(type: ChatEventType, data: String, timestamp: DateTime? = nil) {
|
||||
id = UUID().uuidString
|
||||
self.type = type
|
||||
self.data = data
|
||||
self.timestamp = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
enum ChatEventType: String, Codable, CaseIterable {
|
||||
case message
|
||||
case attachment
|
||||
case event
|
||||
case ping
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// ChatManager.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Apollo
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
// MARK: - ChatManager
|
||||
|
||||
public class ChatManager: ObservableObject {
|
||||
public static let shared = ChatManager()
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@Published public private(set) var sessions: [SessionViewModel] = []
|
||||
@Published public private(set) var currentSession: SessionViewModel?
|
||||
@Published public private(set) var messages: [String: [ChatMessage]] = [:]
|
||||
@Published public private(set) var isLoading = false
|
||||
@Published public private(set) var error: Error?
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let apolloClient: ApolloClient
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
private init(apolloClient: ApolloClient = QLService.shared.client) {
|
||||
self.apolloClient = apolloClient
|
||||
}
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
public func createSession(
|
||||
workspaceId: String,
|
||||
promptName: String = "",
|
||||
docId: String? = nil,
|
||||
pinned: Bool = false
|
||||
) async throws -> SessionViewModel {
|
||||
isLoading = true
|
||||
error = nil
|
||||
|
||||
do {
|
||||
let input = CreateChatSessionInput(
|
||||
docId: docId.map { .some($0) } ?? .null,
|
||||
pinned: .some(pinned),
|
||||
promptName: promptName,
|
||||
workspaceId: workspaceId
|
||||
)
|
||||
|
||||
let mutation = CreateCopilotSessionMutation(options: input)
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
apolloClient.perform(mutation: mutation) { result in
|
||||
switch result {
|
||||
case let .success(graphQLResult):
|
||||
guard let sessionId = graphQLResult.data?.createCopilotSession else {
|
||||
continuation.resume(throwing: ChatError.invalidResponse)
|
||||
return
|
||||
}
|
||||
|
||||
let session = SessionViewModel(
|
||||
id: sessionId,
|
||||
workspaceId: workspaceId,
|
||||
docId: docId,
|
||||
promptName: promptName,
|
||||
model: nil,
|
||||
pinned: pinned,
|
||||
tokens: 0,
|
||||
createdAt: DateTime(date: Date()),
|
||||
updatedAt: DateTime(date: Date()),
|
||||
parentSessionId: nil
|
||||
)
|
||||
|
||||
Task { @MainActor in
|
||||
self.sessions.append(session)
|
||||
self.currentSession = session
|
||||
self.messages[sessionId] = []
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
continuation.resume(returning: session)
|
||||
|
||||
case let .failure(error):
|
||||
Task { @MainActor in
|
||||
self.error = error
|
||||
self.isLoading = false
|
||||
}
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.error = error
|
||||
self.isLoading = false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func sendMessage(
|
||||
content: String,
|
||||
attachments: [String] = [],
|
||||
sessionId: String? = nil
|
||||
) async throws {
|
||||
guard let targetSessionId = sessionId ?? currentSession?.id else {
|
||||
throw ChatError.noActiveSession
|
||||
}
|
||||
|
||||
isLoading = true
|
||||
error = nil
|
||||
|
||||
// Add user message immediately
|
||||
let userMessage = ChatMessage(
|
||||
id: UUID().uuidString,
|
||||
role: .user,
|
||||
content: content,
|
||||
attachments: attachments.isEmpty ? nil : attachments,
|
||||
params: nil,
|
||||
createdAt: DateTime(date: Date())
|
||||
)
|
||||
|
||||
await MainActor.run {
|
||||
var sessionMessages = self.messages[targetSessionId] ?? []
|
||||
sessionMessages.append(userMessage)
|
||||
self.messages[targetSessionId] = sessionMessages
|
||||
}
|
||||
|
||||
do {
|
||||
let input = CreateChatMessageInput(
|
||||
attachments: attachments.isEmpty ? .null : .some(attachments),
|
||||
blobs: .null,
|
||||
content: .some(content),
|
||||
params: .null,
|
||||
sessionId: targetSessionId
|
||||
)
|
||||
|
||||
let mutation = CreateCopilotMessageMutation(options: input)
|
||||
|
||||
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
||||
apolloClient.perform(mutation: mutation) { result in
|
||||
switch result {
|
||||
case let .success(graphQLResult):
|
||||
guard let messageId = graphQLResult.data?.createCopilotMessage else {
|
||||
continuation.resume(throwing: ChatError.invalidResponse)
|
||||
return
|
||||
}
|
||||
|
||||
// Add assistant message placeholder
|
||||
let assistantMessage = ChatMessage(
|
||||
id: messageId,
|
||||
role: .assistant,
|
||||
content: "Thinking...",
|
||||
attachments: nil,
|
||||
params: nil,
|
||||
createdAt: DateTime(date: Date())
|
||||
)
|
||||
|
||||
Task { @MainActor in
|
||||
var sessionMessages = self.messages[targetSessionId] ?? []
|
||||
sessionMessages.append(assistantMessage)
|
||||
self.messages[targetSessionId] = sessionMessages
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
continuation.resume()
|
||||
|
||||
// TODO: Implement streaming response handling
|
||||
|
||||
case let .failure(error):
|
||||
Task { @MainActor in
|
||||
self.error = error
|
||||
self.isLoading = false
|
||||
}
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
self.error = error
|
||||
self.isLoading = false
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
public func switchToSession(_ session: SessionViewModel) {
|
||||
currentSession = session
|
||||
}
|
||||
|
||||
public func deleteSession(sessionId: String) {
|
||||
sessions.removeAll { $0.id == sessionId }
|
||||
messages.removeValue(forKey: sessionId)
|
||||
|
||||
if currentSession?.id == sessionId {
|
||||
currentSession = sessions.first
|
||||
}
|
||||
}
|
||||
|
||||
public func clearError() {
|
||||
error = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ChatError
|
||||
|
||||
public enum ChatError: LocalizedError {
|
||||
case noActiveSession
|
||||
case invalidResponse
|
||||
case networkError(Error)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noActiveSession:
|
||||
"No active chat session"
|
||||
case .invalidResponse:
|
||||
"Invalid response from server"
|
||||
case let .networkError(error):
|
||||
"Network error: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// ChatMessage.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import AffineGraphQL
|
||||
import Foundation
|
||||
|
||||
public struct ChatMessage: Codable, Identifiable, Equatable, Hashable {
|
||||
public var id: String?
|
||||
public var role: MessageRole
|
||||
public var content: String
|
||||
public var attachments: [String]?
|
||||
public var params: [String: String]?
|
||||
public var createdAt: DateTime?
|
||||
|
||||
public var createdDate: Date? {
|
||||
createdAt?.decoded
|
||||
}
|
||||
|
||||
public var messageId: String {
|
||||
id ?? UUID().uuidString
|
||||
}
|
||||
|
||||
public init(
|
||||
id: String? = nil,
|
||||
role: MessageRole,
|
||||
content: String,
|
||||
attachments: [String]? = nil,
|
||||
params: [String: String]? = nil,
|
||||
createdAt: DateTime? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.role = role
|
||||
self.content = content
|
||||
self.attachments = attachments
|
||||
self.params = params
|
||||
self.createdAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
public extension ChatMessage {
|
||||
enum MessageRole: String, Codable, CaseIterable {
|
||||
case user
|
||||
case assistant
|
||||
case system
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionViewModel: Codable, Identifiable, Equatable, Hashable {
|
||||
public var id: String
|
||||
public var workspaceId: String
|
||||
public var docId: String?
|
||||
public var promptName: String
|
||||
public var model: String?
|
||||
public var pinned: Bool
|
||||
public var tokens: Int
|
||||
public var createdAt: DateTime?
|
||||
public var updatedAt: DateTime?
|
||||
public var parentSessionId: String?
|
||||
|
||||
public var createdDate: Date? {
|
||||
createdAt?.decoded
|
||||
}
|
||||
|
||||
public var updatedDate: Date? {
|
||||
updatedAt?.decoded
|
||||
}
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
workspaceId: String,
|
||||
docId: String? = nil,
|
||||
promptName: String,
|
||||
model: String? = nil,
|
||||
pinned: Bool,
|
||||
tokens: Int,
|
||||
createdAt: DateTime? = nil,
|
||||
updatedAt: DateTime? = nil,
|
||||
parentSessionId: String? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.workspaceId = workspaceId
|
||||
self.docId = docId
|
||||
self.promptName = promptName
|
||||
self.model = model
|
||||
self.pinned = pinned
|
||||
self.tokens = tokens
|
||||
self.createdAt = createdAt
|
||||
self.updatedAt = updatedAt
|
||||
self.parentSessionId = parentSessionId
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import ApolloAPI
|
||||
import Foundation
|
||||
|
||||
/// A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
|
||||
extension DateTime {
|
||||
extension DateTime {
|
||||
private static let formatter: DateFormatter = {
|
||||
let fmt = DateFormatter()
|
||||
@@ -20,8 +19,11 @@ extension DateTime {
|
||||
return fmt
|
||||
}()
|
||||
|
||||
init(date: Date) {
|
||||
self.init(Self.formatter.string(from: date))
|
||||
}
|
||||
|
||||
var decoded: Date? {
|
||||
return Self.formatter.date(from: self)
|
||||
Self.formatter.date(from: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Foundation
|
||||
public final class QLService {
|
||||
public static let shared = QLService()
|
||||
private var endpointURL: URL
|
||||
public private(set) var client: ApolloClient
|
||||
public var client: ApolloClient
|
||||
|
||||
private init() {
|
||||
let store = ApolloStore()
|
||||
|
||||
@@ -75,7 +75,48 @@ extension MainViewController: InputBoxDelegate {
|
||||
}
|
||||
|
||||
func inputBoxDidSend(_ inputBox: InputBox) {
|
||||
print(#function, inputBox, inputBox.viewModel)
|
||||
let inputData = inputBox.inputBoxData
|
||||
|
||||
Task { @MainActor in
|
||||
do {
|
||||
let chatManager = ChatManager.shared
|
||||
|
||||
if let currentSession = chatManager.currentSession {
|
||||
try await chatManager.sendMessage(
|
||||
content: inputData.text,
|
||||
attachments: [], // TODO: Handle attachments
|
||||
sessionId: currentSession.id
|
||||
)
|
||||
} else {
|
||||
guard let workspaceId = IntelligentContext.shared.webViewMetadata[.currentWorkspaceId] as? String,
|
||||
!workspaceId.isEmpty
|
||||
else {
|
||||
showAlert(title: "Error", message: "No workspace available")
|
||||
return
|
||||
}
|
||||
|
||||
let session = try await chatManager.createSession(workspaceId: workspaceId)
|
||||
|
||||
try await chatManager.sendMessage(
|
||||
content: inputData.text,
|
||||
attachments: [], // TODO: Handle attachments
|
||||
sessionId: session.id
|
||||
)
|
||||
}
|
||||
|
||||
inputBox.text = ""
|
||||
inputBox.viewModel.clearAllAttachments()
|
||||
|
||||
} catch {
|
||||
showAlert(title: "Error", message: error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func showAlert(title: String, message: String) {
|
||||
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
||||
present(alert, animated: true)
|
||||
}
|
||||
|
||||
func inputBoxTextDidChange(_ text: String) {
|
||||
|
||||
@@ -10,6 +10,27 @@ class MainViewController: UIViewController {
|
||||
$0.delegate = self
|
||||
}
|
||||
|
||||
lazy var tableView = UITableView().then {
|
||||
$0.backgroundColor = .clear
|
||||
$0.separatorStyle = .none
|
||||
$0.delegate = self
|
||||
$0.dataSource = self
|
||||
$0.register(ChatCell.self, forCellReuseIdentifier: "ChatCell")
|
||||
$0.keyboardDismissMode = .interactive
|
||||
$0.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
||||
lazy var emptyStateView = UIView().then {
|
||||
$0.isHidden = true
|
||||
}
|
||||
|
||||
lazy var emptyStateLabel = UILabel().then {
|
||||
$0.text = "Start a conversation..."
|
||||
$0.font = .systemFont(ofSize: 18, weight: .medium)
|
||||
$0.textColor = .systemGray
|
||||
$0.textAlignment = .center
|
||||
}
|
||||
|
||||
lazy var inputBox = InputBox().then {
|
||||
$0.delegate = self
|
||||
}
|
||||
@@ -28,8 +49,10 @@ class MainViewController: UIViewController {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var messages: [ChatMessage] = []
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let intelligentContext = IntelligentContext.shared
|
||||
private let chatManager = ChatManager.shared
|
||||
var terminateEditGesture: UITapGestureRecognizer!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
@@ -38,21 +61,46 @@ class MainViewController: UIViewController {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .affineLayerBackgroundPrimary
|
||||
|
||||
let inputBox = InputBox().then {
|
||||
$0.delegate = self
|
||||
}
|
||||
self.inputBox = inputBox
|
||||
setupUI()
|
||||
setupBindings()
|
||||
|
||||
view.isUserInteractionEnabled = true
|
||||
terminateEditGesture = UITapGestureRecognizer(target: self, action: #selector(terminateEditing))
|
||||
view.addGestureRecognizer(terminateEditGesture)
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
view.addSubview(headerView)
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(emptyStateView)
|
||||
view.addSubview(inputBox)
|
||||
view.addSubview(documentPickerHideDetector)
|
||||
view.addSubview(documentPickerView)
|
||||
|
||||
emptyStateView.addSubview(emptyStateLabel)
|
||||
|
||||
headerView.snp.makeConstraints { make in
|
||||
make.top.equalTo(view.safeAreaLayoutGuide)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
}
|
||||
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalTo(headerView.snp.bottom)
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(inputBox.snp.top)
|
||||
}
|
||||
|
||||
emptyStateView.snp.makeConstraints { make in
|
||||
make.center.equalTo(tableView)
|
||||
make.width.lessThanOrEqualTo(tableView).inset(32)
|
||||
}
|
||||
|
||||
emptyStateLabel.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
inputBox.snp.makeConstraints { make in
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.bottom.equalTo(view.keyboardLayoutGuide.snp.top)
|
||||
@@ -67,10 +115,24 @@ class MainViewController: UIViewController {
|
||||
make.leading.trailing.equalToSuperview()
|
||||
make.height.equalTo(500)
|
||||
}
|
||||
}
|
||||
|
||||
view.isUserInteractionEnabled = true
|
||||
terminateEditGesture = UITapGestureRecognizer(target: self, action: #selector(terminateEditing))
|
||||
view.addGestureRecognizer(terminateEditGesture)
|
||||
private func setupBindings() {
|
||||
chatManager.$currentSession
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] session in
|
||||
self?.updateMessages(for: session?.id)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
chatManager.$messages
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
if let sessionId = self?.chatManager.currentSession?.id {
|
||||
self?.updateMessages(for: sessionId)
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
@@ -90,4 +152,66 @@ class MainViewController: UIViewController {
|
||||
@objc func terminateEditing() {
|
||||
view.endEditing(true)
|
||||
}
|
||||
|
||||
// MARK: - Chat Methods
|
||||
|
||||
private func updateMessages(for sessionId: String?) {
|
||||
guard let sessionId else {
|
||||
messages = []
|
||||
updateEmptyState()
|
||||
tableView.reloadData()
|
||||
return
|
||||
}
|
||||
|
||||
messages = chatManager.messages[sessionId] ?? []
|
||||
updateEmptyState()
|
||||
tableView.reloadData()
|
||||
|
||||
if !messages.isEmpty {
|
||||
let indexPath = IndexPath(row: messages.count - 1, section: 0)
|
||||
tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateEmptyState() {
|
||||
emptyStateView.isHidden = !messages.isEmpty
|
||||
tableView.isHidden = messages.isEmpty
|
||||
}
|
||||
|
||||
// MARK: - Internal Methods for Preview/Testing
|
||||
|
||||
#if DEBUG
|
||||
func setMessagesForPreview(_ previewMessages: [ChatMessage]) {
|
||||
messages = previewMessages
|
||||
updateEmptyState()
|
||||
tableView.reloadData()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
extension MainViewController: UITableViewDataSource {
|
||||
func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
|
||||
messages.count
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "ChatCell", for: indexPath) as! ChatCell
|
||||
let message = messages[indexPath.row]
|
||||
cell.configure(with: message)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
extension MainViewController: UITableViewDelegate {
|
||||
func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat {
|
||||
UITableView.automaticDimension
|
||||
}
|
||||
|
||||
func tableView(_: UITableView, estimatedHeightForRowAt _: IndexPath) -> CGFloat {
|
||||
60
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// ChatCell.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import SnapKit
|
||||
import Then
|
||||
import UIKit
|
||||
|
||||
class ChatCell: UITableViewCell {
|
||||
// MARK: - UI Components
|
||||
|
||||
private lazy var avatarImageView = UIImageView().then {
|
||||
$0.contentMode = .scaleAspectFit
|
||||
$0.layer.cornerRadius = 16
|
||||
$0.layer.cornerCurve = .continuous
|
||||
$0.clipsToBounds = true
|
||||
$0.backgroundColor = .systemGray5
|
||||
}
|
||||
|
||||
private lazy var messageContainerView = UIView().then {
|
||||
$0.layer.cornerRadius = 12
|
||||
$0.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
private lazy var messageLabel = UILabel().then {
|
||||
$0.numberOfLines = 0
|
||||
$0.font = .systemFont(ofSize: 16)
|
||||
$0.textColor = .label
|
||||
}
|
||||
|
||||
private lazy var timestampLabel = UILabel().then {
|
||||
$0.font = .systemFont(ofSize: 12)
|
||||
$0.textColor = .systemGray
|
||||
$0.textAlignment = .right
|
||||
}
|
||||
|
||||
private lazy var stackView = UIStackView().then {
|
||||
$0.axis = .horizontal
|
||||
$0.spacing = 12
|
||||
$0.alignment = .top
|
||||
}
|
||||
|
||||
private lazy var messageStackView = UIStackView().then {
|
||||
$0.axis = .vertical
|
||||
$0.spacing = 4
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var message: ChatMessage?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
setupUI()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder _: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
private func setupUI() {
|
||||
backgroundColor = .clear
|
||||
selectionStyle = .none
|
||||
|
||||
contentView.addSubview(stackView)
|
||||
|
||||
messageStackView.addArrangedSubview(messageContainerView)
|
||||
messageStackView.addArrangedSubview(timestampLabel)
|
||||
|
||||
messageContainerView.addSubview(messageLabel)
|
||||
|
||||
stackView.addArrangedSubview(avatarImageView)
|
||||
stackView.addArrangedSubview(messageStackView)
|
||||
|
||||
stackView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(16)
|
||||
}
|
||||
|
||||
avatarImageView.snp.makeConstraints { make in
|
||||
make.size.equalTo(32)
|
||||
}
|
||||
|
||||
messageLabel.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview().inset(12)
|
||||
}
|
||||
|
||||
messageStackView.snp.makeConstraints { make in
|
||||
make.width.lessThanOrEqualTo(250)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Configuration
|
||||
|
||||
func configure(with message: ChatMessage) {
|
||||
self.message = message
|
||||
|
||||
messageLabel.text = message.content
|
||||
|
||||
if let createdDate = message.createdDate {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .none
|
||||
formatter.timeStyle = .short
|
||||
timestampLabel.text = formatter.string(from: createdDate)
|
||||
} else {
|
||||
timestampLabel.text = ""
|
||||
}
|
||||
|
||||
switch message.role {
|
||||
case .user:
|
||||
configureUserMessage()
|
||||
case .assistant:
|
||||
configureAssistantMessage()
|
||||
case .system:
|
||||
configureSystemMessage()
|
||||
}
|
||||
}
|
||||
|
||||
private func configureUserMessage() {
|
||||
// User message - align to right
|
||||
stackView.semanticContentAttribute = .forceRightToLeft
|
||||
messageContainerView.backgroundColor = .systemBlue
|
||||
messageLabel.textColor = .white
|
||||
avatarImageView.image = UIImage(systemName: "person.circle.fill")
|
||||
avatarImageView.tintColor = .systemBlue
|
||||
timestampLabel.textAlignment = .left
|
||||
}
|
||||
|
||||
private func configureAssistantMessage() {
|
||||
// Assistant message - align to left
|
||||
stackView.semanticContentAttribute = .forceLeftToRight
|
||||
messageContainerView.backgroundColor = .systemGray6
|
||||
messageLabel.textColor = .label
|
||||
avatarImageView.image = UIImage(systemName: "brain.head.profile")
|
||||
avatarImageView.tintColor = .systemPurple
|
||||
timestampLabel.textAlignment = .right
|
||||
}
|
||||
|
||||
private func configureSystemMessage() {
|
||||
// System message - center aligned
|
||||
stackView.semanticContentAttribute = .forceLeftToRight
|
||||
messageContainerView.backgroundColor = .systemYellow.withAlphaComponent(0.3)
|
||||
messageLabel.textColor = .label
|
||||
avatarImageView.image = UIImage(systemName: "gear")
|
||||
avatarImageView.tintColor = .systemOrange
|
||||
timestampLabel.textAlignment = .center
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// AttachmentCellViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AttachmentCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .attachment
|
||||
var id: String
|
||||
var attachments: [AttachmentViewModel]
|
||||
var parentMessageId: String
|
||||
}
|
||||
|
||||
struct AttachmentViewModel: Codable, Identifiable, Equatable, Hashable {
|
||||
var id: String
|
||||
var url: String
|
||||
var mimeType: String?
|
||||
var fileName: String?
|
||||
var size: Int64?
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// CellType.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum CellType: String, Codable, CaseIterable {
|
||||
case userMessage
|
||||
case assistantMessage
|
||||
case systemMessage
|
||||
case attachment
|
||||
case contextReference
|
||||
case workflowStatus
|
||||
case transcription
|
||||
case loading
|
||||
case error
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// ChatCellViewModels.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol ChatCellViewModel: Codable, Identifiable, Equatable, Hashable {
|
||||
var cellType: CellType { get }
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
struct UserMessageCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .userMessage
|
||||
var id: String
|
||||
var content: String
|
||||
var attachments: [AttachmentViewModel]
|
||||
var timestamp: Date?
|
||||
var isRetrying: Bool
|
||||
}
|
||||
|
||||
struct AssistantMessageCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .assistantMessage
|
||||
var id: String
|
||||
var content: String
|
||||
var attachments: [AttachmentViewModel]
|
||||
var timestamp: Date?
|
||||
var isStreaming: Bool
|
||||
var model: String?
|
||||
var tokens: Int?
|
||||
var canRetry: Bool
|
||||
}
|
||||
|
||||
struct SystemMessageCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .systemMessage
|
||||
var id: String
|
||||
var content: String
|
||||
var timestamp: Date?
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// ContextReferenceCellViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ContextReferenceCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .contextReference
|
||||
var id: String
|
||||
var references: [ChatManager.ContextReference]
|
||||
var parentMessageId: String
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// ErrorCellViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ErrorCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .error
|
||||
var id: String
|
||||
var errorMessage: String
|
||||
var canRetry: Bool
|
||||
var retryAction: String?
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// LoadingCellViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LoadingCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .loading
|
||||
var id: String
|
||||
var message: String?
|
||||
var progress: Double?
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// WorkflowStatusCellViewModel.swift
|
||||
// Intelligents
|
||||
//
|
||||
// Created by 秋星桥 on 6/26/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct WorkflowStatusCellViewModel: ChatCellViewModel {
|
||||
var cellType: CellType = .workflowStatus
|
||||
var id: String
|
||||
var workflow: ChatManager.WorkflowEventData
|
||||
var parentMessageId: String
|
||||
}
|
||||
@@ -66,6 +66,12 @@ public class InputBoxViewModel: ObservableObject {
|
||||
.assign(to: \.canSend, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
public func clearAllAttachments() {
|
||||
imageAttachments.removeAll()
|
||||
fileAttachments.removeAll()
|
||||
documentAttachments.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Text Management
|
||||
|
||||
Reference in New Issue
Block a user