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

View File

@@ -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";

View File

@@ -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,

View File

@@ -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 {
}
}
}

View File

@@ -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!
}
}

View File

@@ -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 {
}
}
}

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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()
// }
// }
//}
// }

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -39,4 +39,3 @@ public enum CustomJSON: CustomScalarType, Hashable {
hasher.combine(_jsonValue)
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)"
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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?
}

View File

@@ -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
}

View File

@@ -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?
}

View File

@@ -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
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -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
}

View File

@@ -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