Download apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMermaidBlockView.swift from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMermaidBlockView.swift
- Command line
-
hf download hf://SaylorTwift/openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMermaidBlockView.swift
-
curl -L -o ChatMermaidBlockView.swift https://huggingface.co/SaylorTwift/openclaw/resolve/main/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMermaidBlockView.swift
12.3 kB
| #if canImport(WebKit) && (os(iOS) || os(macOS)) | |
| import SwiftUI | |
| import WebKit | |
| #if os(macOS) | |
| import AppKit | |
| #else | |
| import UIKit | |
| #endif | |
| struct ChatMermaidBlockView: View { | |
| let source: String | |
| (\.self) private var environment | |
| (\.displayScale) private var displayScale | |
| (\.colorScheme) private var colorScheme | |
| private var width = 0 | |
| private var result: Result<ChatMermaidRenderedImage, ChatMermaidFailure>? | |
| private var token: UUID? | |
| private var generation = UUID() | |
| private var showSource = false | |
| private var isHovered = false | |
| /// Keep the selected preview stable while rotation re-renders the inline diagram. | |
| private var expanded: PreviewSelection? | |
| private struct PreviewSelection: Identifiable { | |
| let id = UUID() | |
| let svg: String | |
| let background: String | |
| } | |
| var body: some View { | |
| VStack(alignment: .leading, spacing: 0) { | |
| HStack(spacing: 0) { | |
| Spacer() | |
| ChatCopyButton(text: self.source, label: "Copy diagram source", revealed: self.isHovered) | |
| Menu { | |
| #if os(macOS) | |
| Button { | |
| ChatPasteboard.copy(self.source) | |
| } label: { | |
| Text("Copy diagram source").font(OpenClawChatTypography.body) | |
| } | |
| #endif | |
| Button { | |
| self.showSource.toggle() | |
| } label: { | |
| if self.showSource { | |
| Text("View diagram").font(OpenClawChatTypography.body) | |
| } else { | |
| Text("View source").font(OpenClawChatTypography.body) | |
| } | |
| } | |
| Button { | |
| self.expand() | |
| } label: { | |
| Text("Expand diagram").font(OpenClawChatTypography.body) | |
| } | |
| .disabled(self.rendered == nil) | |
| if self.canRetry { | |
| Button { | |
| self.render() | |
| } label: { | |
| Text("Retry diagram").font(OpenClawChatTypography.body) | |
| } | |
| } | |
| } label: { | |
| Image(systemName: "ellipsis") | |
| .frame(width: ChatCopyButton.controlSize, height: ChatCopyButton.controlSize) | |
| } | |
| .accessibilityLabel("Diagram options") | |
| #if os(macOS) | |
| .menuStyle(.borderlessButton) | |
| .menuIndicator(.hidden) | |
| .fixedSize() | |
| .help("Diagram options") | |
| #endif | |
| } | |
| .foregroundStyle(.secondary) | |
| if self.showSource { | |
| self.sourceView | |
| } else if let rendered = self.rendered { | |
| Button { | |
| self.expand() | |
| } label: { | |
| OpenClawPlatformImageFactory.image(rendered.image) | |
| .resizable() | |
| .aspectRatio(rendered.size.width / rendered.size.height, contentMode: .fit) | |
| .frame(maxWidth: .infinity) | |
| .padding(8) | |
| } | |
| .buttonStyle(.plain) | |
| .accessibilityLabel("Mermaid diagram") | |
| .accessibilityHint("Expand diagram") | |
| } else { | |
| VStack(alignment: .leading, spacing: 8) { | |
| self.status | |
| .font(OpenClawChatTypography.caption) | |
| .foregroundStyle(.secondary) | |
| self.sourceView | |
| } | |
| .padding(10) | |
| } | |
| } | |
| .frame(maxWidth: .infinity, alignment: .leading) | |
| .background(OpenClawChatTheme.assistantBubble) | |
| .clipShape(RoundedRectangle(cornerRadius: 12)) | |
| .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.secondary.opacity(0.2))) | |
| .onGeometryChange(for: Int.self) { geometry in | |
| Int(geometry.size.width.rounded(.up)) | |
| } action: { self.width = $0 } | |
| .onChange(of: self.request, initial: true) { _, _ in self.render() } | |
| .onDisappear { self.cancel() } | |
| .onHover { self.isHovered = $0 } | |
| #if os(macOS) | |
| .sheet(item: self.$expanded) { self.preview($0) } | |
| #else | |
| .fullScreenCover(item: self.$expanded) { self.preview($0) } | |
| #endif | |
| } | |
| private func preview(_ selected: PreviewSelection) -> some View { | |
| ChatMermaidPreviewView(svg: selected.svg, background: selected.background) | |
| } | |
| private func expand() { | |
| guard let rendered = self.rendered else { return } | |
| self.expanded = PreviewSelection( | |
| svg: rendered.svg, | |
| background: self.cssColor(OpenClawChatTheme.assistantBubble)) | |
| } | |
| private var sourceView: some View { | |
| ChatCodeBlockView(block: ChatCodeBlock(language: nil, code: self.source, isComplete: true)) | |
| } | |
| private var rendered: ChatMermaidRenderedImage? { | |
| guard case let .success(image)? = self.result else { return nil } | |
| return image | |
| } | |
| private var canRetry: Bool { | |
| guard case let .failure(error)? = self.result else { return false } | |
| switch error { | |
| case .busy, .unavailable, .timedOut, .invalidResult: return true | |
| case let .rendering(_, retryable): return retryable | |
| case .invalidRequest, .tooLarge: return false | |
| } | |
| } | |
| private var status: some View { | |
| if self.result == nil { | |
| Text("Rendering diagram…") | |
| } else if self.canRetry { | |
| Text( | |
| "Diagram temporarily unavailable. Use the menu to retry, or read and copy its source.") | |
| } else { | |
| Text( | |
| "Diagram unavailable. Check the syntax or simplify the diagram. You can still read or copy its source.") | |
| } | |
| } | |
| private var request: ChatMermaidRequest? { | |
| guard self.width > 0 else { return nil } | |
| return ChatMermaidRequest( | |
| source: self.source, | |
| width: self.width, | |
| displayScale: self.displayScale, | |
| theme: ChatMermaidTheme( | |
| background: self.cssColor(OpenClawChatTheme.assistantBubble), | |
| foreground: self.cssColor(OpenClawChatTheme.assistantText), | |
| muted: self.cssColor(.secondary), | |
| border: self.cssColor(OpenClawChatTheme.divider), | |
| accent: self.cssColor(OpenClawChatTheme.accent), | |
| fontFamily: "sans-serif", | |
| darkMode: self.colorScheme == .dark)) | |
| } | |
| private func cssColor(_ color: Color) -> String { | |
| let resolved = color.resolve(in: self.environment) | |
| return String( | |
| format: "#%02x%02x%02x", | |
| Int(resolved.red * 255), | |
| Int(resolved.green * 255), | |
| Int(resolved.blue * 255)) | |
| } | |
| private func render() { | |
| self.cancel() | |
| self.result = nil | |
| guard let request = self.request else { return } | |
| let generation = self.generation | |
| self.token = ChatMermaidRenderer.shared.render(request) { result in | |
| guard self.generation == generation else { return } | |
| self.result = result | |
| } | |
| } | |
| private func cancel() { | |
| self.generation = UUID() | |
| if let token = self.token { | |
| self.token = nil | |
| ChatMermaidRenderer.shared.cancel(token) | |
| } | |
| } | |
| } | |
| private struct ChatMermaidPreviewView: View { | |
| let svg: String | |
| let background: String | |
| (\.dismiss) private var dismiss | |
| private var failed = false | |
| var body: some View { | |
| VStack(spacing: 0) { | |
| HStack { | |
| Spacer() | |
| Button { | |
| self.dismiss() | |
| } label: { | |
| Image(systemName: "xmark") | |
| .frame(width: 44, height: 44) | |
| } | |
| .buttonStyle(.plain) | |
| .accessibilityLabel("Close diagram preview") | |
| #if os(macOS) | |
| .keyboardShortcut(.cancelAction) | |
| #endif | |
| } | |
| if self.failed { | |
| Text("Diagram preview unavailable") | |
| .font(OpenClawChatTypography.body) | |
| .frame(maxWidth: .infinity, maxHeight: .infinity) | |
| } else { | |
| ChatMermaidSvgView(svg: self.svg, background: self.background, onFailure: { self.failed = true }) | |
| } | |
| } | |
| .background(OpenClawChatTheme.assistantBubble) | |
| #if os(macOS) | |
| .frame(minWidth: 500, idealWidth: 900, minHeight: 350, idealHeight: 600) | |
| // Mac sheets default to a form width; fit both axes so Expand honors the ideal size. | |
| .presentationSizing(.fitted) | |
| #endif | |
| } | |
| } | |
| private struct ChatMermaidSvgView { | |
| let svg: String | |
| let background: String | |
| let onFailure: () -> Void | |
| func makeCoordinator() -> Coordinator { | |
| Coordinator(onFailure: self.onFailure) | |
| } | |
| func makeWebView(coordinator: Coordinator) -> WKWebView { | |
| let configuration = WKWebViewConfiguration() | |
| configuration.websiteDataStore = .nonPersistent() | |
| configuration.defaultWebpagePreferences.allowsContentJavaScript = false | |
| let view = WKWebView(frame: .zero, configuration: configuration) | |
| view.navigationDelegate = coordinator | |
| view.allowsLinkPreview = false | |
| view.underPageBackgroundColor = .clear | |
| #if os(macOS) | |
| view.allowsMagnification = true | |
| #else | |
| view.isOpaque = false | |
| view.backgroundColor = .clear | |
| #endif | |
| let encoded = Data(self.svg.utf8).base64EncodedString() | |
| // Script-free SVG stays an image; auto margins center short diagrams without clipping tall ones. | |
| view.loadHTMLString(""" | |
| <!doctype html><html><head><meta charset="utf-8"> | |
| <meta http-equiv="Content-Security-Policy" content="default-src 'none'; | |
| img-src data:; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=8, user-scalable=yes"> | |
| <style> | |
| html,body{margin:0;height:100%;background:\(self.background)} | |
| body{display:flex} | |
| img{display:block;width:100%;height:auto;margin:auto 0} | |
| </style> | |
| </head><body><img alt="" src="data:image/svg+xml;base64,\(encoded)"></body></html> | |
| """, baseURL: nil) | |
| return view | |
| } | |
| final class Coordinator: NSObject, WKNavigationDelegate { | |
| private let onFailure: () -> Void | |
| init(onFailure: @escaping () -> Void) { | |
| self.onFailure = onFailure | |
| } | |
| func webViewWebContentProcessDidTerminate(_: WKWebView) { | |
| self.onFailure() | |
| } | |
| func webView(_: WKWebView, didFail _: WKNavigation!, withError _: any Error) { | |
| self.onFailure() | |
| } | |
| func webView(_: WKWebView, didFailProvisionalNavigation _: WKNavigation!, withError _: any Error) { | |
| self.onFailure() | |
| } | |
| } | |
| } | |
| #if os(macOS) | |
| extension ChatMermaidSvgView: NSViewRepresentable { | |
| func makeNSView(context: Context) -> WKWebView { | |
| self.makeWebView(coordinator: context.coordinator) | |
| } | |
| func updateNSView(_: WKWebView, context: Context) {} | |
| static func dismantleNSView(_ view: WKWebView, coordinator: Coordinator) { | |
| view.navigationDelegate = nil | |
| view.stopLoading() | |
| } | |
| } | |
| #else | |
| extension ChatMermaidSvgView: UIViewRepresentable { | |
| func makeUIView(context: Context) -> WKWebView { | |
| self.makeWebView(coordinator: context.coordinator) | |
| } | |
| func updateUIView(_: WKWebView, context: Context) {} | |
| static func dismantleUIView(_ view: WKWebView, coordinator: Coordinator) { | |
| view.navigationDelegate = nil | |
| view.stopLoading() | |
| } | |
| } | |
| #endif | |
| #endif | |