import SwiftUI // MARK: - Markdown renderer for chat assistant messages struct ChatMarkdownView: View { let markdown: String var body: some View { VStack(alignment: .leading, spacing: 6) { ForEach(Array(ChatMarkdown.parse(markdown).enumerated()), id: \.offset) { _, block in blockView(block) } } .environment(\.openURL, OpenURLAction { url in guard let safe = safeWebURL(url.absoluteString) else { return .discarded } NSWorkspace.shared.open(safe) return .handled }) } @ViewBuilder private func blockView(_ block: MDBlock) -> some View { switch block { case .heading(let level, let text): Text(inlineAttributed(text)) .font(.system(size: level <= 2 ? 14 : 13, weight: level <= 2 ? .bold : .semibold)) .foregroundColor(Color(hex: "#F1F2F4")) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) case .paragraph(let text): Text(inlineAttributed(text)) .font(.system(size: 12.5)) .foregroundColor(Color(hex: "#B0B5BE")) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) case .codeBlock(_, let code): HStack(alignment: .top) { Text(code) .font(.system(size: 11, design: .monospaced)) .foregroundColor(Color(hex: "#C8CDD4")) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) CopyButton(text: code) } .padding(10) .background(Color(hex: "#0D0E12")) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay(RoundedRectangle(cornerRadius: 8).stroke(Color.white.opacity(0.08), lineWidth: 1)) case .listItem(let prefix, let text, let indent): HStack(alignment: .top, spacing: 6) { Text(prefix) .font(.system(size: 12.5)) .foregroundColor(Color(hex: "#6B7079")) .frame(minWidth: prefix.count > 2 ? 20 : 10, alignment: .leading) Text(inlineAttributed(text)) .font(.system(size: 12.5)) .foregroundColor(Color(hex: "#B0B5BE")) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) } .padding(.leading, CGFloat(indent) * 12) case .quote(let text): HStack(alignment: .top, spacing: 8) { Rectangle() .fill(Color(hex: "#4B5563")) .frame(width: 2) .clipShape(Capsule()) Text(inlineAttributed(text)) .font(.system(size: 12.5)) .foregroundColor(Color(hex: "#8A8F98")) .fixedSize(horizontal: false, vertical: true) .textSelection(.enabled) } case .rule: Divider().opacity(0.3) } } private func inlineAttributed(_ text: String) -> AttributedString { var options = AttributedString.MarkdownParsingOptions( interpretedSyntax: .inlineOnlyPreservingWhitespace) options.failurePolicy = .returnPartiallyParsedIfPossible return (try? AttributedString(markdown: text, options: options)) ?? AttributedString(text) } } // MARK: - Copy button private struct CopyButton: View { let text: String @State private var copied = false var body: some View { Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) withAnimation { copied = true } DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { withAnimation { copied = false } } } label: { Image(systemName: copied ? "checkmark" : "doc.on.doc") .font(.system(size: 10)) .foregroundColor(copied ? Color(hex: "#22C55E") : Color(hex: "#6B7079")) } .buttonStyle(.plain) .frame(width: 22, height: 22) .background(Color.white.opacity(0.06)) .clipShape(RoundedRectangle(cornerRadius: 4)) } }