fix(ios): stabilize streaming layout updates
This commit is contained in:
parent
9c37ce5f9e
commit
b76dd70375
@ -1,12 +1,19 @@
|
||||
import Foundation
|
||||
|
||||
struct AssistantTextSegment: Identifiable {
|
||||
enum Kind {
|
||||
struct AssistantTextSegment: Identifiable, Equatable {
|
||||
enum Kind: Equatable {
|
||||
case thinking
|
||||
case response
|
||||
|
||||
var stableIDPrefix: String {
|
||||
switch self {
|
||||
case .thinking: "t"
|
||||
case .response: "r"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id = UUID()
|
||||
let id: String
|
||||
let kind: Kind
|
||||
let text: String
|
||||
}
|
||||
@ -16,7 +23,7 @@ enum AssistantTextParser {
|
||||
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return [] }
|
||||
guard raw.contains("<") else {
|
||||
return [AssistantTextSegment(kind: .response, text: trimmed)]
|
||||
return [AssistantTextSegment(id: "r0", kind: .response, text: trimmed)]
|
||||
}
|
||||
|
||||
var segments: [AssistantTextSegment] = []
|
||||
@ -51,7 +58,7 @@ enum AssistantTextParser {
|
||||
}
|
||||
|
||||
guard matchedTag else {
|
||||
return [AssistantTextSegment(kind: .response, text: trimmed)]
|
||||
return [AssistantTextSegment(id: "r0", kind: .response, text: trimmed)]
|
||||
}
|
||||
|
||||
if includeThinking {
|
||||
@ -146,6 +153,11 @@ enum AssistantTextParser {
|
||||
{
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
segments.append(AssistantTextSegment(kind: kind, text: trimmed))
|
||||
let nextIndex = segments.count
|
||||
segments.append(
|
||||
AssistantTextSegment(
|
||||
id: "\(kind.stableIDPrefix)\(nextIndex)",
|
||||
kind: kind,
|
||||
text: trimmed))
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ public struct OpenClawChatView: View {
|
||||
@State private var hasPerformedInitialScroll = false
|
||||
@State private var isPinnedToBottom = true
|
||||
@State private var lastUserMessageID: UUID?
|
||||
@State private var pendingStreamingScrollTask: Task<Void, Never>?
|
||||
private let showsSessionSwitcher: Bool
|
||||
private let style: Style
|
||||
private let markdownVariant: ChatMarkdownVariant
|
||||
@ -83,6 +84,10 @@ public struct OpenClawChatView: View {
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.onAppear { self.viewModel.load() }
|
||||
.onDisappear {
|
||||
self.pendingStreamingScrollTask?.cancel()
|
||||
self.pendingStreamingScrollTask = nil
|
||||
}
|
||||
.sheet(isPresented: self.$showSessions) {
|
||||
if self.showsSessionSwitcher {
|
||||
ChatSessionsSheet(viewModel: self.viewModel)
|
||||
@ -180,6 +185,16 @@ public struct OpenClawChatView: View {
|
||||
}
|
||||
.onChange(of: self.viewModel.streamingAssistantText) { _, _ in
|
||||
guard self.hasPerformedInitialScroll, self.isPinnedToBottom else { return }
|
||||
self.scheduleStreamingScrollToBottom()
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleStreamingScrollToBottom() {
|
||||
guard self.pendingStreamingScrollTask == nil else { return }
|
||||
self.pendingStreamingScrollTask = Task { @MainActor in
|
||||
defer { self.pendingStreamingScrollTask = nil }
|
||||
try? await Task.sleep(nanoseconds: 100_000_000)
|
||||
guard !Task.isCancelled, self.hasPerformedInitialScroll, self.isPinnedToBottom else { return }
|
||||
withAnimation(.snappy(duration: 0.22)) {
|
||||
self.scrollPosition = self.scrollerBottomID
|
||||
}
|
||||
|
||||
@ -48,4 +48,13 @@ import Testing
|
||||
#expect(AssistantTextParser.hasVisibleContent(in: "<think>internal</think>") == false)
|
||||
#expect(AssistantTextParser.hasVisibleContent(in: "<think>internal</think>", includeThinking: true))
|
||||
}
|
||||
|
||||
@Test func usesStableSegmentIDsAcrossRepeatedStreamingParses() {
|
||||
let raw = "<think>internal</think>\n\n<final>Hello there</final>"
|
||||
let first = AssistantTextParser.segments(from: raw, includeThinking: true)
|
||||
let second = AssistantTextParser.segments(from: raw, includeThinking: true)
|
||||
|
||||
#expect(first.map(\.id) == ["t0", "r1"])
|
||||
#expect(first == second)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user