feat: fix several view model issue (#13388)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Error messages in chat cells are now clearly displayed with improved
formatting and dynamic height adjustment for better readability.
* Introduced the ability to remove specific chat cell view models from a
session.

* **Bug Fixes**
* Enhanced error handling to automatically remove invalid chat cell view
models when a message creation fails.

* **Other Improvements**
* Improved internal logic for handling message attachments and added
more detailed debug logging for the copilot response lifecycle.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Lakr
2025-08-01 15:24:33 +08:00
committed by GitHub
parent 5cbcf6f907
commit 1661ab1790
3 changed files with 53 additions and 8 deletions

View File

@@ -160,6 +160,7 @@ private extension ChatManager {
viewModelId: UUID
) {
assert(!Thread.isMainThread)
print("[+] starting copilot response for session: \(sessionId)")
let messageParameters: [String: AnyHashable] = [
// packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
@@ -167,11 +168,11 @@ private extension ChatManager {
"files": [String](), // attachment in context, keep nil for now
"searchMode": editorData.isSearchEnabled ? "MUST" : "AUTO",
]
let hasMultipleAttachmentBlobs = [
let attachmentCount = [
editorData.fileAttachments.count,
editorData.documentAttachments.count,
].reduce(0, +) > 1
let attachmentFieldName = hasMultipleAttachmentBlobs ? "options.blobs" : "options.blob"
].reduce(0, +)
let attachmentFieldName = attachmentCount > 1 && attachmentCount != 0 ? "options.blobs" : "options.blob"
let uploadableAttachments: [GraphQLFile] = [
editorData.fileAttachments.map { file -> GraphQLFile in
.init(fieldName: attachmentFieldName, originalName: file.name, data: file.data ?? .init())
@@ -183,8 +184,8 @@ private extension ChatManager {
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
guard let input = try? CreateChatMessageInput(
attachments: [],
blob: hasMultipleAttachmentBlobs ? .none : "",
blobs: hasMultipleAttachmentBlobs ? .some([]) : .none,
blob: .none,
blobs: attachmentCount > 1 && attachmentCount != 0 ? .some([]) : .none,
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
sessionId: sessionId
@@ -195,11 +196,13 @@ private extension ChatManager {
}
let mutation = CreateCopilotMessageMutation(options: input)
QLService.shared.client.upload(operation: mutation, files: uploadableAttachments) { result in
print("[*] createCopilotMessage result: \(result)")
DispatchQueue.main.async {
switch result {
case let .success(graphQLResult):
guard let messageIdentifier = graphQLResult.data?.createCopilotMessage else {
self.report(sessionId, ChatError.invalidResponse)
self.delete(sessionId: sessionId, vmId: viewModelId)
return
}
self.startStreamingResponse(
@@ -217,6 +220,7 @@ private extension ChatManager {
private extension ChatManager {
func startStreamingResponse(sessionId: String, messageId: String, applyingTo vmId: UUID) {
print("[+] starting streaming response for session: \(sessionId), message: \(messageId)")
let base = IntelligentContext.shared.webViewMetadata[.currentServerBaseUrl] as? String
guard let base, let url = URL(string: base) else {
report(sessionId, ChatError.invalidServerConfiguration)

View File

@@ -68,6 +68,10 @@ public class ChatManager: ObservableObject, @unchecked Sendable {
}
}
}
public func delete(sessionId: String, vmId: UUID) {
with(sessionId: sessionId) { $0.removeValue(forKey: vmId) }
}
@discardableResult
public func append(sessionId: String, _ viewModel: any ChatCellViewModel) -> UUID {

View File

@@ -11,24 +11,61 @@ import Then
import UIKit
class ErrorCell: ChatBaseCell {
let label = UILabel()
override func prepareContentView(inside contentView: UIView) {
super.prepareContentView(inside: contentView)
contentView.addSubview(label)
}
override func prepareForReuse() {
super.prepareForReuse()
label.attributedText = nil
}
override func layoutContentView(bounds: CGRect) {
super.layoutContentView(bounds: bounds)
let width = bounds.width * 0.8
label.frame = .init(
x: (bounds.width - width) / 2,
y: 0,
width: width,
height: bounds.height
)
}
override func configure(with viewModel: any ChatCellViewModel) {
super.configure(with: viewModel)
guard let vm = viewModel as? ErrorCellViewModel else {
assertionFailure("Invalid view model type")
return
}
label.attributedText = Self.attributeText(for: vm.errorMessage)
}
static func attributeText(for text: String) -> NSAttributedString {
return .init(string: text, attributes: [
.font: UIFont.preferredFont(forTextStyle: .footnote),
.foregroundColor: UIColor.affineTextSecondary,
.paragraphStyle: NSMutableParagraphStyle().then {
$0.lineBreakMode = .byWordWrapping
$0.alignment = .center
}
])
}
override class func heightForContent(
for viewModel: any ChatCellViewModel,
width: CGFloat
) -> CGFloat {
_ = viewModel
_ = width
return 0
let vm = viewModel as! ErrorCellViewModel
let text = Self.attributeText(for: vm.errorMessage)
let boundingRect = text.boundingRect(
with: CGSize(width: width * 0.8, height: .greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
context: nil
)
let boundingSize = boundingRect.size
return ceil(boundingSize.height)
}
}