import SwiftUI import Textual public enum ChatMarkdownVariant: String, CaseIterable, Sendable { case standard case compact } @MainActor 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 { // First extract any MEDIA: audio references let audioResult = InlineAudioParser.parse(self.text) // Then process images from the remaining text let processed = ChatMarkdownPreprocessor.preprocess(markdown: audioResult.cleaned) VStack(alignment: .leading, spacing: 10) { // Only render text if there's content after processing if !processed.cleaned.isEmpty { StructuredText(markdown: processed.cleaned) .modifier(ChatMarkdownStyle( variant: self.variant, context: self.context, font: self.font, textColor: self.textColor)) } // Render inline audio players if !audioResult.audioFiles.isEmpty { InlineAudioList(audioFiles: audioResult.audioFiles) } // Render inline images 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)) } } @MainActor private struct InlineImageList: View { let images: [ChatMarkdownPreprocessor.InlineImage] var body: some View { ForEach(images, id: \.id) { item in if let img = item.image { MoltbotPlatformImageFactory.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) } } } }