Spaces:
Paused
Paused
| import SwiftUI | |
| import Textual | |
| public enum ChatMarkdownVariant: String, CaseIterable, Sendable { | |
| case standard | |
| case compact | |
| } | |
| struct ChatMarkdownRenderer: View { | |
| enum Context { | |
| case user | |
| case assistant | |
| } | |
| let text: String | |
| let context: Context | |
| let variant: ChatMarkdownVariant | |
| let font: Font | |
| let textColor: Color | |
| var body: some View { | |
| let processed = ChatMarkdownPreprocessor.preprocess(markdown: self.text) | |
| VStack(alignment: .leading, spacing: 10) { | |
| StructuredText(markdown: processed.cleaned) | |
| .modifier(ChatMarkdownStyle( | |
| variant: self.variant, | |
| context: self.context, | |
| font: self.font, | |
| textColor: self.textColor)) | |
| if !processed.images.isEmpty { | |
| InlineImageList(images: processed.images) | |
| } | |
| } | |
| } | |
| } | |
| private struct ChatMarkdownStyle: ViewModifier { | |
| let variant: ChatMarkdownVariant | |
| let context: ChatMarkdownRenderer.Context | |
| let font: Font | |
| let textColor: Color | |
| func body(content: Content) -> some View { | |
| Group { | |
| if self.variant == .compact { | |
| content.textual.structuredTextStyle(.default) | |
| } else { | |
| content.textual.structuredTextStyle(.gitHub) | |
| } | |
| } | |
| .font(self.font) | |
| .foregroundStyle(self.textColor) | |
| .textual.inlineStyle(self.inlineStyle) | |
| .textual.textSelection(.enabled) | |
| } | |
| private var inlineStyle: InlineStyle { | |
| let linkColor: Color = self.context == .user ? self.textColor : .accentColor | |
| let codeScale: CGFloat = self.variant == .compact ? 0.85 : 0.9 | |
| return InlineStyle() | |
| .code(.monospaced, .fontScale(codeScale)) | |
| .link(.foregroundColor(linkColor)) | |
| } | |
| } | |
| private struct InlineImageList: View { | |
| let images: [ChatMarkdownPreprocessor.InlineImage] | |
| var body: some View { | |
| ForEach(images, id: \.id) { item in | |
| if let img = item.image { | |
| OpenClawPlatformImageFactory.image(img) | |
| .resizable() | |
| .scaledToFit() | |
| .frame(maxHeight: 260) | |
| .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) | |
| .overlay( | |
| RoundedRectangle(cornerRadius: 12, style: .continuous) | |
| .strokeBorder(Color.white.opacity(0.12), lineWidth: 1)) | |
| } else { | |
| Text(item.label.isEmpty ? "Image" : item.label) | |
| .font(.footnote) | |
| .foregroundStyle(.secondary) | |
| } | |
| } | |
| } | |
| } | |