fix(chat-ui): avoid animated initial scroll
parent
5adec0eae0
commit
4021da524c
|
|
@ -10,6 +10,7 @@ public struct ClawdisChatView: View {
|
||||||
@State private var viewModel: ClawdisChatViewModel
|
@State private var viewModel: ClawdisChatViewModel
|
||||||
@State private var scrollerBottomID = UUID()
|
@State private var scrollerBottomID = UUID()
|
||||||
@State private var showSessions = false
|
@State private var showSessions = false
|
||||||
|
@State private var hasPerformedInitialScroll = false
|
||||||
private let showsSessionSwitcher: Bool
|
private let showsSessionSwitcher: Bool
|
||||||
private let style: Style
|
private let style: Style
|
||||||
|
|
||||||
|
|
@ -67,44 +68,59 @@ public struct ClawdisChatView: View {
|
||||||
|
|
||||||
private var messageList: some View {
|
private var messageList: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ZStack {
|
||||||
LazyVStack(spacing: Layout.messageSpacing) {
|
ScrollView {
|
||||||
ForEach(self.visibleMessages) { msg in
|
LazyVStack(spacing: Layout.messageSpacing) {
|
||||||
ChatMessageBubble(message: msg, style: self.style)
|
ForEach(self.visibleMessages) { msg in
|
||||||
.frame(
|
ChatMessageBubble(message: msg, style: self.style)
|
||||||
maxWidth: .infinity,
|
.frame(
|
||||||
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
|
maxWidth: .infinity,
|
||||||
}
|
alignment: msg.role.lowercased() == "user" ? .trailing : .leading)
|
||||||
|
}
|
||||||
|
|
||||||
if self.viewModel.pendingRunCount > 0 {
|
if self.viewModel.pendingRunCount > 0 {
|
||||||
ChatTypingIndicatorBubble(style: self.style)
|
ChatTypingIndicatorBubble(style: self.style)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.viewModel.pendingToolCalls.isEmpty {
|
if !self.viewModel.pendingToolCalls.isEmpty {
|
||||||
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
|
ChatPendingToolsBubble(toolCalls: self.viewModel.pendingToolCalls)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let text = self.viewModel.streamingAssistantText, !text.isEmpty {
|
if let text = self.viewModel.streamingAssistantText, !text.isEmpty {
|
||||||
ChatStreamingAssistantBubble(text: text)
|
ChatStreamingAssistantBubble(text: text)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
Color.clear
|
Color.clear
|
||||||
.frame(height: 1)
|
.frame(height: 1)
|
||||||
.id(self.scrollerBottomID)
|
.id(self.scrollerBottomID)
|
||||||
|
}
|
||||||
|
.padding(.top, Layout.messageListPaddingTop)
|
||||||
|
.padding(.bottom, Layout.messageListPaddingBottom)
|
||||||
|
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
||||||
}
|
}
|
||||||
.padding(.top, Layout.messageListPaddingTop)
|
|
||||||
.padding(.bottom, Layout.messageListPaddingBottom)
|
if self.viewModel.isLoading {
|
||||||
.padding(.horizontal, Layout.messageListPaddingHorizontal)
|
ProgressView()
|
||||||
|
.controlSize(.large)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: self.viewModel.isLoading) { _, isLoading in
|
||||||
|
guard !isLoading, !self.hasPerformedInitialScroll else { return }
|
||||||
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
||||||
|
self.hasPerformedInitialScroll = true
|
||||||
}
|
}
|
||||||
.onChange(of: self.viewModel.messages.count) { _, _ in
|
.onChange(of: self.viewModel.messages.count) { _, _ in
|
||||||
|
guard self.hasPerformedInitialScroll else { return }
|
||||||
withAnimation(.snappy(duration: 0.22)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onChange(of: self.viewModel.pendingRunCount) { _, _ in
|
.onChange(of: self.viewModel.pendingRunCount) { _, _ in
|
||||||
|
guard self.hasPerformedInitialScroll else { return }
|
||||||
withAnimation(.snappy(duration: 0.22)) {
|
withAnimation(.snappy(duration: 0.22)) {
|
||||||
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
proxy.scrollTo(self.scrollerBottomID, anchor: .bottom)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue