From 167c6d87eb175e76d0cb9a6a728b6c8075d090e8 Mon Sep 17 00:00:00 2001 From: Hendrik Mennen Date: Tue, 30 Jun 2026 12:50:17 +0200 Subject: [PATCH 1/2] progres --- src/OneWare.Chat/ChatHeightEstimation.cs | 34 ++ .../ChatMessageAssistantViewModel.cs | 5 +- .../ChatMessageReasoningViewModel.cs | 11 +- .../ChatMessages/ChatMessageToolViewModel.cs | 11 +- .../ChatMessages/ChatMessageUserViewModel.cs | 5 +- src/OneWare.Chat/Views/ChatView.axaml | 112 ++-- .../EstimatingVirtualizingStackPanel.cs | 532 ++++++++++++++++++ .../Controls/IEstimatedHeightItem.cs | 15 + 8 files changed, 666 insertions(+), 59 deletions(-) create mode 100644 src/OneWare.Chat/ChatHeightEstimation.cs create mode 100644 src/OneWare.Essentials/Controls/EstimatingVirtualizingStackPanel.cs create mode 100644 src/OneWare.Essentials/Controls/IEstimatedHeightItem.cs diff --git a/src/OneWare.Chat/ChatHeightEstimation.cs b/src/OneWare.Chat/ChatHeightEstimation.cs new file mode 100644 index 000000000..50c6ff007 --- /dev/null +++ b/src/OneWare.Chat/ChatHeightEstimation.cs @@ -0,0 +1,34 @@ +using System; + +namespace OneWare.Chat; + +/// +/// Heuristics for estimating a chat message's rendered pixel height from its markdown content, +/// before it has been realized and measured. Used to give the virtualizing message list a stable, +/// content-accurate scroll extent. +/// +public static class ChatHeightEstimation +{ + private const double CharWidth = 7.5; + private const double LineHeight = 18; + private const double VerticalPadding = 16; + + /// + /// Estimates the height of a block of markdown/plain text for the given available width by + /// approximating wrapped line counts. The real measured height replaces this once realized, + /// so the estimate only needs to be in the right ballpark. + /// + public static double EstimateMarkdown(string? text, double width, double extra = 0) + { + if (string.IsNullOrEmpty(text)) + return VerticalPadding + LineHeight; + + var charsPerLine = Math.Max(1, width / CharWidth); + double lines = 0; + + foreach (var hardLine in text.Split('\n')) + lines += Math.Max(1, Math.Ceiling(hardLine.Length / charsPerLine)); + + return VerticalPadding + extra + lines * LineHeight; + } +} diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs index ed165d0b6..e67e8839a 100644 --- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs +++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageAssistantViewModel.cs @@ -1,9 +1,10 @@ using System.Runtime.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using OneWare.Essentials.Controls; namespace OneWare.Chat.ViewModels.ChatMessages; -public class ChatMessageAssistantViewModel : ObservableObject, IChatMessage +public class ChatMessageAssistantViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem { public ChatMessageAssistantViewModel(string? messageId = null) { @@ -27,4 +28,6 @@ public bool IsStreaming get; set => SetProperty(ref field, value); } + + public double EstimateHeight(double width) => ChatHeightEstimation.EstimateMarkdown(Content, width); } diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs index 5249f1cb4..6e4f56a9e 100644 --- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs +++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageReasoningViewModel.cs @@ -1,9 +1,10 @@ using System.Runtime.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using OneWare.Essentials.Controls; namespace OneWare.Chat.ViewModels.ChatMessages; -public class ChatMessageReasoningViewModel : ObservableObject, IChatMessage +public class ChatMessageReasoningViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem { public ChatMessageReasoningViewModel(string? reasoningId = null) { @@ -27,4 +28,12 @@ public bool IsStreaming get; set => SetProperty(ref field, value); } + + public double EstimateHeight(double width) + { + const double header = 36; + if (!IsStreaming) + return header; + return header + System.Math.Min(200, ChatHeightEstimation.EstimateMarkdown(Content, width)) + 8; + } } diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs index b52ff6215..7fa55b719 100644 --- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs +++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageToolViewModel.cs @@ -1,9 +1,10 @@ using System.Runtime.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using OneWare.Essentials.Controls; namespace OneWare.Chat.ViewModels.ChatMessages; -public class ChatMessageToolViewModel : ObservableObject, IChatMessage +public class ChatMessageToolViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem { public ChatMessageToolViewModel(string id, string toolName) { @@ -34,4 +35,12 @@ public bool IsSuccessful get; set => SetProperty(ref field, value); } + + public double EstimateHeight(double width) + { + const double header = 36; + if (!IsToolRunning) + return header; + return header + System.Math.Min(200, ChatHeightEstimation.EstimateMarkdown(ToolOutput, width)) + 8; + } } diff --git a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs index 98b817c04..9687baf80 100644 --- a/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs +++ b/src/OneWare.Chat/ViewModels/ChatMessages/ChatMessageUserViewModel.cs @@ -1,9 +1,10 @@ using System.Runtime.Serialization; using CommunityToolkit.Mvvm.ComponentModel; +using OneWare.Essentials.Controls; namespace OneWare.Chat.ViewModels.ChatMessages; -public class ChatMessageUserViewModel : ObservableObject, IChatMessage +public class ChatMessageUserViewModel : ObservableObject, IChatMessage, IEstimatedHeightItem { public ChatMessageUserViewModel(string message) { @@ -15,4 +16,6 @@ public ChatMessageUserViewModel(string message) public string Message { get; } public DateTimeOffset Timestamp { get; } + + public double EstimateHeight(double width) => ChatHeightEstimation.EstimateMarkdown(Message, width); } diff --git a/src/OneWare.Chat/Views/ChatView.axaml b/src/OneWare.Chat/Views/ChatView.axaml index aba6e18d4..ca6681db1 100644 --- a/src/OneWare.Chat/Views/ChatView.axaml +++ b/src/OneWare.Chat/Views/ChatView.axaml @@ -75,65 +75,67 @@ - - - - - - - - - - - - - - + + + + + + - - - - - - + + - - - + + + + + + + + + - - + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + +