import ComposableArchitecture import Foundation import MarkdownUI import SharedUIComponents import SwiftUI struct BotMessage: View { var r: Double { messageBubbleCornerRadius } let id: String let text: String let references: [DisplayedChatMessage.Reference] let chat: StoreOf @Environment(\.colorScheme) var colorScheme @AppStorage(\.chatFontSize) var chatFontSize @AppStorage(\.chatCodeFontSize) var chatCodeFontSize @State var isReferencesPresented = false @State var isReferencesHovered = false var body: some View { HStack(alignment: .bottom, spacing: 2) { VStack(alignment: .leading, spacing: 16) { if !references.isEmpty { Button(action: { isReferencesPresented.toggle() }, label: { HStack(spacing: 4) { Image(systemName: "plus.circle") Text("Used \(references.count) references") } .padding(8) .background { RoundedRectangle(cornerRadius: r - 4) .foregroundStyle(Color(isReferencesHovered ? .black : .clear)) } .overlay { RoundedRectangle(cornerRadius: r - 4) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } .foregroundStyle(.secondary) }) .buttonStyle(.plain) .popover(isPresented: $isReferencesPresented, arrowEdge: .trailing) { ReferenceList(references: references, chat: chat) } } Markdown(text) .textSelection(.enabled) .markdownTheme(.custom(fontSize: chatFontSize)) .markdownCodeSyntaxHighlighter( ChatCodeSyntaxHighlighter( brightMode: colorScheme != .dark, fontSize: chatCodeFontSize ) ) } .frame(alignment: .trailing) .padding() .background { RoundedCorners(tl: r, tr: r, bl: 0, br: r) .fill(Color.contentBackground) } .overlay { RoundedCorners(tl: r, tr: r, bl: 0, br: r) .stroke(Color(nsColor: .separatorColor), lineWidth: 1) } .padding(.leading, 8) .shadow(color: .black.opacity(0.1), radius: 2) .contextMenu { Button("Copy") { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) } Button("Set as Extra System Prompt") { chat.send(.setAsExtraPromptButtonTapped(id)) } Divider() Button("Delete") { chat.send(.deleteMessageButtonTapped(id)) } } CopyButton { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 2) } } struct ReferenceList: View { let references: [DisplayedChatMessage.Reference] let chat: StoreOf var body: some View { ScrollView { VStack(alignment: .leading, spacing: 8) { ForEach(0..