Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/OneWare.Chat/ChatHeightEstimation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;

namespace OneWare.Chat;

/// <summary>
/// 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.
/// </summary>
public static class ChatHeightEstimation
{
private const double CharWidth = 7.5;
private const double LineHeight = 18;
private const double VerticalPadding = 16;

/// <summary>
/// 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.
/// </summary>
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;
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -27,4 +28,6 @@ public bool IsStreaming
get;
set => SetProperty(ref field, value);
}

public double EstimateHeight(double width) => ChatHeightEstimation.EstimateMarkdown(Content, width);
}
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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)
{
Expand All @@ -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);
}
113 changes: 58 additions & 55 deletions src/OneWare.Chat/Views/ChatView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,65 +75,68 @@
</Border>

<Panel Grid.Row="1">
<ScrollViewer Name="ScrollViewer" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Interaction.Behaviors>
<AutoScrollDuringDragBehavior ScrollDelta="4" />
</Interaction.Behaviors>
<Border Padding="10 10 10 4">
<Grid RowDefinitions="Auto, Auto, Auto" RowSpacing="5">
<ItemsControl Name="ItemsControl" ItemsSource="{Binding Messages}">
<ItemsControl.Styles>
<Style Selector="Expander /template/ ToggleButton">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}" />
<Setter Property="CornerRadius" Value="3" />
</Style>
<Style Selector="Expander /template/ ToggleButton:pointerover /template/ Border">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}" />
</Style>
<Style Selector="Expander /template/ ToggleButton:pointerover /template/ Border Border">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle /template/ Border">
<Setter Property="CornerRadius" Value="3" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle /template/ Grid > Border">
<Setter Property="Margin" Value="0 2" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ItemsControl.Styles>
<DockPanel>
<!-- Loading indicator + queued messages are pinned below the conversation and kept
outside the scrolling region so the virtualizing message list above can virtualize
against the viewport (it must be the direct content of the ScrollViewer). -->
<StackPanel DockPanel.Dock="Bottom" Margin="10 0 10 4" Spacing="5">
<!-- Loading Indicator -->
<Grid ColumnDefinitions="Auto, Auto" ColumnSpacing="5"
IsVisible="{Binding IsBusy}">
<controls:Spinner Width="12" Height="12" VerticalAlignment="Center" />

<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBlock Grid.Column="1"
Foreground="{DynamicResource ThemeForegroundLowBrush}" FontSize="11"
FontStyle="Italic"
Text="{Binding WorkingStatusText}" />
</Grid>

<!-- Loading Indicator -->
<Grid Grid.Row="1" ColumnDefinitions="Auto, Auto" ColumnSpacing="5"
IsVisible="{Binding IsBusy}">
<controls:Spinner Width="12" Height="12" VerticalAlignment="Center" />
<!-- Queued messages (dimmed, pinned below the conversation) -->
<ItemsControl Opacity="0.5" ItemsSource="{Binding QueuedMessages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>

<TextBlock Grid.Column="1"
Foreground="{DynamicResource ThemeForegroundLowBrush}" FontSize="11"
FontStyle="Italic"
Text="{Binding WorkingStatusText}" />
</Grid>
<ScrollViewer Name="ScrollViewer" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Interaction.Behaviors>
<AutoScrollDuringDragBehavior ScrollDelta="4" />
</Interaction.Behaviors>

<ItemsControl Name="ItemsControl" ItemsSource="{Binding Messages}" Margin="10 0">
<ItemsControl.Styles>
<Style Selector="Expander /template/ ToggleButton">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}" />
<Setter Property="CornerRadius" Value="3" />
</Style>
<Style Selector="Expander /template/ ToggleButton:pointerover /template/ Border">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}" />
</Style>
<Style Selector="Expander /template/ ToggleButton:pointerover /template/ Border Border">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle /template/ Border">
<Setter Property="CornerRadius" Value="3" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Expander /template/ ToggleButton#PART_toggle /template/ Grid > Border">
<Setter Property="Margin" Value="0 2" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
</ItemsControl.Styles>

<!-- Queued messages (dimmed, pinned below the conversation) -->
<ItemsControl Grid.Row="2" Opacity="0.5"
ItemsSource="{Binding QueuedMessages}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</Border>
</ScrollViewer>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:EstimatingVirtualizingStackPanel Spacing="5"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</DockPanel>

<!--
<Border VerticalAlignment="Center" HorizontalAlignment="Center"
Expand Down
Loading
Loading