Files
openclaw/apps/shared/OpenClawKit/Sources/OpenClawChatUI/ChatMarkdownBlockViews.swift

191 lines
6.4 KiB
Swift

import SwiftMath
import SwiftUI
#if os(macOS)
import AppKit
#else
import UIKit
#endif
@MainActor
struct ChatCodeBlockView: View {
let block: ChatCodeBlock
var body: some View {
VStack(alignment: .leading, spacing: 6) {
if let language = self.block.language {
Text(language)
.font(OpenClawChatTypography.caption2)
.foregroundStyle(.secondary)
}
ScrollView(.horizontal, showsIndicators: false) {
Text(self.attributedCode)
.font(OpenClawChatTypography.mono(size: 13, relativeTo: .footnote))
.foregroundStyle(OpenClawChatTheme.assistantText)
.lineSpacing(2)
.textSelection(.enabled)
}
}
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(OpenClawChatTheme.subtleCard)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1)))
}
private var attributedCode: AttributedString {
// Open (still-streaming) fences skip highlighting so each delta stays
// cheap; completed blocks hit the content-keyed highlight cache.
guard self.block.isComplete else { return AttributedString(self.block.code) }
return ChatCodeHighlightCache.highlighted(
code: self.block.code,
languageId: self.block.language)
}
}
@MainActor
struct ChatMathBlockView: View {
let block: ChatMathBlock
let textColor: Color
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = OpenClawChatTypography.bodySize
var body: some View {
if self.block.isComplete,
ChatMathParseCache.mathList(latex: self.block.latex) != nil
{
ScrollView(.horizontal, showsIndicators: false) {
ChatMathPlatformView(
latex: self.block.latex,
fontSize: self.fontSize,
textColor: self.textColor)
.fixedSize()
.accessibilityElement(children: .ignore)
.accessibilityLabel(Text(self.block.latex))
}
.defaultScrollAnchor(.center)
.frame(maxWidth: .infinity)
.padding(.vertical, 4)
} else {
ChatCodeBlockView(block: ChatCodeBlock(
language: nil,
code: self.block.latex,
isComplete: false))
}
}
}
#if os(macOS)
@MainActor
private struct ChatMathPlatformView: NSViewRepresentable {
let latex: String
let fontSize: CGFloat
let textColor: Color
func makeNSView(context: Context) -> MTMathUILabel {
MTMathUILabel()
}
func updateNSView(_ view: MTMathUILabel, context: Context) {
self.configure(view)
}
private func configure(_ view: MTMathUILabel) {
view.displayErrorInline = false
view.labelMode = .display
view.textAlignment = .center
view.fontSize = self.fontSize
view.textColor = NSColor(self.textColor)
if view.latex != self.latex {
view.latex = self.latex
}
}
}
#else
@MainActor
private struct ChatMathPlatformView: UIViewRepresentable {
let latex: String
let fontSize: CGFloat
let textColor: Color
func makeUIView(context: Context) -> MTMathUILabel {
MTMathUILabel()
}
func updateUIView(_ view: MTMathUILabel, context: Context) {
self.configure(view)
}
private func configure(_ view: MTMathUILabel) {
view.displayErrorInline = false
view.labelMode = .display
view.textAlignment = .center
view.fontSize = self.fontSize
view.textColor = UIColor(self.textColor)
if view.latex != self.latex {
view.latex = self.latex
}
}
}
#endif
@MainActor
struct ChatMarkdownTableView: View {
let table: ChatMarkdownTable
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
Grid(alignment: .topLeading, horizontalSpacing: 14, verticalSpacing: 7) {
GridRow {
ForEach(self.table.header.indices, id: \.self) { column in
// One cell per column carries the GFM alignment.
self.cell(self.table.header[column], column: column, isHeader: true)
.gridColumnAlignment(self.columnAlignment(column))
}
}
Divider()
ForEach(self.table.rows.indices, id: \.self) { rowIndex in
GridRow {
ForEach(self.table.rows[rowIndex].indices, id: \.self) { column in
self.cell(self.table.rows[rowIndex][column], column: column, isHeader: false)
}
}
}
}
}
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(OpenClawChatTheme.subtleCard)
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), lineWidth: 1)))
}
private func cell(_ text: String, column: Int, isHeader: Bool) -> some View {
Text(self.inlineMarkdown(text))
.font(isHeader ? OpenClawChatTypography.footnoteSemiBold : OpenClawChatTypography.footnote)
.foregroundStyle(OpenClawChatTheme.assistantText)
.textSelection(.enabled)
}
private func inlineMarkdown(_ text: String) -> AttributedString {
let options = AttributedString.MarkdownParsingOptions(
interpretedSyntax: .inlineOnlyPreservingWhitespace,
failurePolicy: .returnPartiallyParsedIfPossible)
return (try? AttributedString(markdown: text, options: options)) ?? AttributedString(text)
}
private func columnAlignment(_ column: Int) -> HorizontalAlignment {
guard column < self.table.alignments.count else { return .leading }
switch self.table.alignments[column] {
case .leading: return .leading
case .center: return .center
case .trailing: return .trailing
}
}
}