HuggingChat-Mac/Views/ResponseView.swift (150 lines of code) (raw):
//
// ResponseView.swift
// HuggingChat-Mac
//
// Created by Cyril Zakka on 10/1/24.
//
import SwiftUI
import MarkdownView
struct CustomImageProvider: ImageDisplayable {
func makeImage(url: URL, alt: String?) -> some View {
AnimatedImageView(imageURL: url)
}
}
struct ResponseView: View {
@Environment(ConversationViewModel.self) private var conversationModel
@Environment(ModelManager.self) private var modelManager
@AppStorage("inlineCodeHiglight") private var inlineCodeHiglight: AccentColorOption = .blue
@AppStorage("lightCodeBlockTheme") private var lightCodeBlockTheme: String = "xcode"
@AppStorage("darkCodeBlockTheme") private var darkCodeBlockTheme: String = "monokai-sublime"
@Binding var isResponseVisible: Bool
@Binding var responseSize: CGSize
var isLocal: Bool = false
var body: some View {
if isLocal {
if !modelManager.outputText.isEmpty {
if isResponseVisible {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading) {
if ThemingEngine.shared.currentTheme.markdownFont == nil {
MarkdownView(text: modelManager.outputText)
.padding(.vertical)
.frame(maxWidth: .infinity, alignment: .leading)
.fontGroup(DefaultFontGroup.automatic)
.markdownRenderingThread(.background)
.tint(inlineCodeHiglight.color, for: .inlineCodeBlock)
.codeHighlighterTheme(CodeHighlighterTheme(lightModeThemeName: lightCodeBlockTheme, darkModeThemeName: darkCodeBlockTheme))
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.id(8)
} else {
MarkdownView(text: modelManager.outputText)
.padding(.vertical)
.frame(maxWidth: .infinity, alignment: .leading)
.markdownRenderingThread(.background)
.fontGroup(ThemingEngine.shared.currentTheme.markdownFont!)
.tint(inlineCodeHiglight.color, for: .inlineCodeBlock)
.codeHighlighterTheme(CodeHighlighterTheme(lightModeThemeName: lightCodeBlockTheme, darkModeThemeName: darkCodeBlockTheme))
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.id(8)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
responseSize.width = newValue.width
responseSize.height = min(max(newValue.height, 20), 320)
}
}
.onChange(of: modelManager.outputText, {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(8, anchor: .bottom)
}
}
})
}
.frame(height: responseSize.height)
.contentMargins(.horizontal, 20, for: .scrollContent)
.scrollIndicators(.hidden)
.background(.ultraThickMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
} else {
if let message = conversationModel.message,
!message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
isResponseVisible {
ScrollViewReader { proxy in
ScrollView {
VStack(alignment: .leading) {
if ThemingEngine.shared.currentTheme.markdownFont == nil {
MarkdownView(text: (conversationModel.imageURL != nil ? ")\n\n\n\n": "") + message.content)
.imageProvider(CustomImageProvider(), forURLScheme: "https")
.padding(.vertical)
.frame(maxWidth: .infinity, alignment: .leading)
.fontGroup(DefaultFontGroup.automatic)
.markdownRenderingThread(.background)
.tint(inlineCodeHiglight.color, for: .inlineCodeBlock)
.codeHighlighterTheme(CodeHighlighterTheme(lightModeThemeName: lightCodeBlockTheme, darkModeThemeName: darkCodeBlockTheme))
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.id(8)
} else {
MarkdownView(text: (conversationModel.imageURL != nil ? ")\n\n\n\n": "") + message.content)
.imageProvider(CustomImageProvider(), forURLScheme: "https")
.padding(.vertical)
.frame(maxWidth: .infinity, alignment: .leading)
.markdownRenderingThread(.background)
.fontGroup(ThemingEngine.shared.currentTheme.markdownFont!)
.tint(inlineCodeHiglight.color, for: .inlineCodeBlock)
.codeHighlighterTheme(CodeHighlighterTheme(lightModeThemeName: lightCodeBlockTheme, darkModeThemeName: darkCodeBlockTheme))
.multilineTextAlignment(.leading)
.textSelection(.enabled)
.id(8)
}
// Sources
if let webSearch = conversationModel.message?.webSearch, webSearch.sources.count > 0 && conversationModel.isInteracting == false {
Divider()
Text("Sources")
.font(.subheadline)
.fontWeight(.semibold)
.padding(.vertical, 5)
ScrollView(.horizontal) {
HStack {
ForEach(webSearch.sources) { source in
LinkPreview(link: source)
.frame(width: 150)
}
}
}
.padding(.bottom)
.scrollIndicators(.hidden)
.scrollClipDisabled()
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.onGeometryChange(for: CGRect.self) { proxy in
proxy.frame(in: .global)
} action: { newValue in
responseSize.width = newValue.width
responseSize.height = min(max(newValue.height, 175), 500)
}
}
// .safeAreaInset(edge: .bottom) {
// if let imageURL = conversationModel.imageURL {
// ResponseToolBar(imageURL: imageURL)
// }
// }
.onChange(of: conversationModel.message?.content, {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(8, anchor: .bottom)
}
}
})
}
.frame(height: responseSize.height)
.contentMargins(.horizontal, 20, for: .scrollContent)
.scrollIndicators(.hidden)
.background(.ultraThickMaterial)
.overlay {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(.secondary.opacity(0.5), lineWidth: 1.0)
}
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
}
}