127 lines
4.5 KiB
Swift
127 lines
4.5 KiB
Swift
import MarkdownUI
|
|
import SwiftUI
|
|
|
|
struct SybilChatTranscriptView: View {
|
|
var messages: [Message]
|
|
var isLoading: Bool
|
|
var isSending: Bool
|
|
|
|
private var hasPendingAssistant: Bool {
|
|
messages.contains { message in
|
|
message.id.hasPrefix("temp-assistant-") && message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
ScrollViewReader { proxy in
|
|
ScrollView {
|
|
LazyVStack(alignment: .leading, spacing: 24) {
|
|
if isLoading && messages.isEmpty {
|
|
Text("Loading messages…")
|
|
.font(.footnote)
|
|
.foregroundStyle(SybilTheme.textMuted)
|
|
.padding(.top, 24)
|
|
}
|
|
|
|
ForEach(messages) { message in
|
|
MessageBubble(message: message, isSending: isSending)
|
|
.id(message.id)
|
|
}
|
|
|
|
if isSending && !hasPendingAssistant {
|
|
HStack(spacing: 8) {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
.tint(SybilTheme.textMuted)
|
|
Text("Assistant is typing…")
|
|
.font(.footnote)
|
|
.foregroundStyle(SybilTheme.textMuted)
|
|
}
|
|
.id("typing-indicator")
|
|
}
|
|
|
|
Color.clear
|
|
.frame(height: 2)
|
|
.id("chat-bottom-anchor")
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 14)
|
|
.padding(.vertical, 18)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.scrollDismissesKeyboard(.interactively)
|
|
.onAppear {
|
|
proxy.scrollTo("chat-bottom-anchor", anchor: .bottom)
|
|
}
|
|
.onChange(of: messages.map(\.id)) { _, _ in
|
|
withAnimation(.easeOut(duration: 0.22)) {
|
|
proxy.scrollTo("chat-bottom-anchor", anchor: .bottom)
|
|
}
|
|
}
|
|
.onChange(of: isSending) { _, _ in
|
|
withAnimation(.easeOut(duration: 0.22)) {
|
|
proxy.scrollTo("chat-bottom-anchor", anchor: .bottom)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct MessageBubble: View {
|
|
var message: Message
|
|
var isSending: Bool
|
|
|
|
private var isUser: Bool {
|
|
message.role == .user
|
|
}
|
|
|
|
private var isPendingAssistant: Bool {
|
|
message.id.hasPrefix("temp-assistant-") &&
|
|
isSending &&
|
|
message.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
}
|
|
|
|
var body: some View {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if isPendingAssistant {
|
|
HStack(spacing: 8) {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
.tint(SybilTheme.textMuted)
|
|
Text("Thinking…")
|
|
.font(.footnote)
|
|
.foregroundStyle(SybilTheme.textMuted)
|
|
}
|
|
.padding(.vertical, 2)
|
|
} else {
|
|
Markdown(message.content)
|
|
.tint(SybilTheme.primary)
|
|
.foregroundStyle(isUser ? SybilTheme.text : SybilTheme.text.opacity(0.95))
|
|
.markdownTextStyle {
|
|
FontSize(15)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, isUser ? 14 : 2)
|
|
.padding(.vertical, isUser ? 11 : 2)
|
|
.background(
|
|
Group {
|
|
if isUser {
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.fill(SybilTheme.userBubble.opacity(0.86))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16)
|
|
.stroke(SybilTheme.primary.opacity(0.45), lineWidth: 1)
|
|
)
|
|
} else {
|
|
RoundedRectangle(cornerRadius: 0)
|
|
.fill(Color.clear)
|
|
}
|
|
}
|
|
)
|
|
.frame(maxWidth: isUser ? 420 : nil, alignment: isUser ? .trailing : .leading)
|
|
}
|
|
}
|
|
}
|