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:
Lakr
2025-06-27 14:36:37 +08:00
committed by GitHub
parent f80b69273f
commit 9a1ce2ba3c
33 changed files with 1182 additions and 155 deletions
@@ -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)
}
}