feat(editor): migrate typst mermaid to native (#14499)

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

* **New Features**
* Native/WASM Mermaid and Typst SVG preview rendering on desktop and
mobile, plus cross-platform Preview plugin integrations.

* **Improvements**
* Centralized, sanitized rendering bridge with automatic Typst
font-directory handling and configurable native renderer selection.
* More consistent and robust error serialization and worker-backed
preview flows for improved stability and performance.

* **Tests**
* Extensive unit and integration tests for preview rendering, font
discovery, sanitization, and error serialization.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-03-20 04:04:40 +08:00
committed by GitHub
parent 16a8f17717
commit 7ac8b14b65
85 changed files with 4926 additions and 835 deletions

View File

@@ -8,4 +8,18 @@
import ApolloAPI
/// The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
public typealias JSON = String
public struct JSON: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
public let value: JSONValue
public init(_jsonValue value: JSONValue) throws {
self.value = value
}
public init(dictionaryLiteral elements: (String, JSONValue)...) {
value = ApolloAPI.JSONObject(uniqueKeysWithValues: elements) as JSONValue
}
public var _jsonValue: JSONValue {
value
}
}

View File

@@ -8,4 +8,22 @@
import ApolloAPI
/// The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
public typealias JSONObject = String
public struct JSONObject: CustomScalarType, Hashable, ExpressibleByDictionaryLiteral {
public let object: ApolloAPI.JSONObject
public init(_jsonValue value: JSONValue) throws {
object = try ApolloAPI.JSONObject(_jsonValue: value)
}
public init(_ object: ApolloAPI.JSONObject) {
self.object = object
}
public init(dictionaryLiteral elements: (String, JSONValue)...) {
object = ApolloAPI.JSONObject(uniqueKeysWithValues: elements)
}
public var _jsonValue: JSONValue {
object
}
}

View File

@@ -26,6 +26,7 @@ private extension InputBoxData {
}
public extension ChatManager {
@MainActor
func startUserRequest(editorData: InputBoxData, sessionId: String) {
append(sessionId: sessionId, UserMessageCellViewModel(
id: .init(),
@@ -163,7 +164,7 @@ private extension ChatManager {
assert(!Thread.isMainThread)
print("[+] starting copilot response for session: \(sessionId)")
let messageParameters: [String: AnyHashable] = [
let messageParameters: AffineGraphQL.JSON = [
// packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx
"docs": editorData.documentAttachments.map(\.documentID), // affine doc
"files": [String](), // attachment in context, keep nil for now
@@ -193,18 +194,14 @@ private extension ChatManager {
},
].flatMap(\.self)
assert(uploadableAttachments.allSatisfy { !($0.data?.isEmpty ?? true) })
guard let input = try? CreateChatMessageInput(
let input = CreateChatMessageInput(
attachments: [],
blob: attachmentCount == 1 ? "" : .none,
blobs: attachmentCount > 1 && attachmentCount != 0 ? .some([]) : .none,
content: .some(contextSnippet.isEmpty ? editorData.text : "\(contextSnippet)\n\(editorData.text)"),
params: .some(AffineGraphQL.JSON(_jsonValue: messageParameters)),
params: .some(messageParameters),
sessionId: sessionId
) else {
report(sessionId, ChatError.unknownError)
assertionFailure() // very unlikely to happen
return
}
)
let mutation = CreateCopilotMessageMutation(options: input)
QLService.shared.client.upload(operation: mutation, files: uploadableAttachments) { result in
print("[*] createCopilotMessage result: \(result)")
@@ -277,7 +274,7 @@ private extension ChatManager {
let eventSource = EventSource()
let dataTask = eventSource.dataTask(for: request)
var document = ""
self.writeMarkdownContent(document + loadingIndicator, sessionId: sessionId, vmId: vmId)
await self.writeMarkdownContent(document + loadingIndicator, sessionId: sessionId, vmId: vmId)
for await event in dataTask.events() {
switch event {
case .open:
@@ -287,7 +284,7 @@ private extension ChatManager {
case let .event(event):
guard let data = event.data else { continue }
document += data
self.writeMarkdownContent(
await self.writeMarkdownContent(
document + loadingIndicator,
sessionId: sessionId,
vmId: vmId
@@ -297,13 +294,13 @@ private extension ChatManager {
print("[*] connection closed")
}
}
self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId)
await self.writeMarkdownContent(document, sessionId: sessionId, vmId: vmId)
self.closeAll()
}))
self.closable.append(closable)
}
private func writeMarkdownContent(
@MainActor private func writeMarkdownContent(
_ document: String,
sessionId: SessionID,
vmId: UUID

View File

@@ -9,7 +9,7 @@ import Foundation
import MarkdownView
extension IntelligentContext {
func prepareMarkdownViewThemes() {
@MainActor func prepareMarkdownViewThemes() {
MarkdownTheme.default.colors.body = .affineTextPrimary
MarkdownTheme.default.colors.highlight = .affineTextLink
}

View File

@@ -40,7 +40,7 @@ struct AssistantMessageCellViewModel: ChatCellViewModel {
var preprocessedContent: MarkdownTextView.PreprocessedContent
init(
@MainActor init(
id: UUID,
content: String,
timestamp: Date,