-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(chat): Show estimated remaining time to run chat script (#33)
- Loading branch information
Showing
6 changed files
with
382 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
using Spectre.Console; | ||
using Spectre.Console.Rendering; | ||
|
||
namespace Raiqub.LlmTools.FlowPair.Support.Console; | ||
|
||
public sealed class LongRemainingTimeColumn : ProgressColumn | ||
{ | ||
private readonly TimeProvider _timeProvider; | ||
private double _lastStep; | ||
private DateTime? _estimatedFinishTime; | ||
|
||
public LongRemainingTimeColumn(TimeProvider? timeProvider = null) | ||
{ | ||
_timeProvider = timeProvider ?? TimeProvider.System; | ||
} | ||
|
||
/// <inheritdoc/> | ||
protected override bool NoWrap => true; | ||
|
||
/// <summary> | ||
/// Gets or sets the style of the remaining time text. | ||
/// </summary> | ||
public Style? Style { get; set; } = Color.Blue; | ||
|
||
/// <inheritdoc/> | ||
public override IRenderable Render(RenderOptions options, ProgressTask task, TimeSpan deltaTime) | ||
{ | ||
var now = _timeProvider.GetLocalNow(); | ||
CollectSample(task, now); | ||
|
||
var remaining = GetRemainingTime(task, now); | ||
if (remaining == null) | ||
{ | ||
return new Markup("--:--:--"); | ||
} | ||
|
||
if (remaining.Value.TotalHours > 99) | ||
{ | ||
return new Markup("**:**:**"); | ||
} | ||
|
||
return new Text($"{remaining.Value:hh\\:mm\\:ss}", Style ?? Style.Plain); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override int? GetColumnWidth(RenderOptions options) => 8; | ||
|
||
private void CollectSample(ProgressTask task, DateTimeOffset now) | ||
{ | ||
if (task.StartTime == null || task.Value.Equals(_lastStep)) | ||
{ | ||
return; | ||
} | ||
|
||
var currTime = now.DateTime; | ||
var timeElapsed = currTime - task.StartTime.Value; | ||
var rateSecs = timeElapsed.TotalSeconds / task.Value; | ||
var remainingSteps = task.MaxValue - task.Value; | ||
var remainingTime = remainingSteps * rateSecs; | ||
|
||
_lastStep = task.Value; | ||
_estimatedFinishTime = currTime + TimeSpan.FromSeconds(remainingTime); | ||
} | ||
|
||
private TimeSpan? GetRemainingTime(ProgressTask task, DateTimeOffset now) | ||
{ | ||
if (task.IsFinished) | ||
{ | ||
return TimeSpan.Zero; | ||
} | ||
|
||
if (_estimatedFinishTime == null) | ||
{ | ||
return null; | ||
} | ||
|
||
var currTime = now.DateTime; | ||
return currTime < _estimatedFinishTime.Value | ||
? _estimatedFinishTime.Value - currTime | ||
: null; | ||
} | ||
} |
169 changes: 169 additions & 0 deletions
169
tests/FlowPair.Tests/Support/Console/LongRemainingTimeColumnTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
using FluentAssertions; | ||
using NSubstitute; | ||
using Raiqub.LlmTools.FlowPair.Support.Console; | ||
using Raiqub.LlmTools.FlowPair.Tests.Testing; | ||
using Spectre.Console; | ||
using Spectre.Console.Rendering; | ||
using Spectre.Console.Testing; | ||
|
||
namespace Raiqub.LlmTools.FlowPair.Tests.Support.Console; | ||
|
||
public sealed class LongRemainingTimeColumnTests : IDisposable | ||
{ | ||
private readonly TimeProvider _timeProvider = Substitute.For<TimeProvider>(); | ||
private readonly TestConsole _testConsole = new(); | ||
private readonly RenderOptions _renderOptions; | ||
|
||
public LongRemainingTimeColumnTests() | ||
{ | ||
_renderOptions = RenderOptions.Create(_testConsole); | ||
_timeProvider.LocalTimeZone.Returns(TimeZoneInfo.Utc); | ||
} | ||
|
||
public void Dispose() => _testConsole.Dispose(); | ||
|
||
[Fact] | ||
public void ConstructorWithNullTimeProviderShouldUseSystemTimeProvider() | ||
{ | ||
var column = new LongRemainingTimeColumn(); | ||
column.Should().NotBeNull(); | ||
var result = column.Render( | ||
options: _renderOptions, | ||
task: new ProgressTask(1, "Test", 100), | ||
deltaTime: TimeSpan.Zero); | ||
|
||
result.GetText().Should().Be("--:--:--"); | ||
} | ||
|
||
[Fact] | ||
public void PropertiesShouldHaveCorrectDefaultValues() | ||
{ | ||
var column = new LongRemainingTimeColumn(); | ||
|
||
column.Style.Should().Be((Style)Color.Blue); | ||
column.GetColumnWidth(_renderOptions).Should().Be(8); | ||
var noWrap = column.GetType() | ||
.GetProperty( | ||
"NoWrap", | ||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) | ||
?.GetValue(column); | ||
noWrap.Should().Be(true); | ||
} | ||
|
||
[Theory] | ||
[InlineData(null)] | ||
[InlineData(0)] | ||
[InlineData(100)] // Task finished | ||
public void RenderSpecialCasesShouldReturnExpectedOutput(int? value) | ||
{ | ||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(startTime.AddMinutes(10))); | ||
|
||
var column = new LongRemainingTimeColumn(_timeProvider); | ||
var task = new ProgressTask(1, "Test", 100, autoStart: false) { Value = value ?? 0 }; | ||
task.Unsafe().StartTime = startTime; | ||
|
||
var result = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
|
||
if (value == 100) | ||
result.GetText().Should().Be("00:00:00"); | ||
else | ||
result.GetText().Should().Be("--:--:--"); | ||
} | ||
|
||
[Theory] | ||
[InlineData(25, 5, "00:15:00")] | ||
[InlineData(50, 10, "00:10:00")] | ||
[InlineData(75, 15, "00:05:00")] | ||
public void RenderTaskInProgressShouldReturnAccurateFormattedTime( | ||
int value, | ||
int elapsedMinutes, | ||
string expected) | ||
{ | ||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
var currentTime = startTime.AddMinutes(elapsedMinutes); | ||
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime)); | ||
|
||
var column = new LongRemainingTimeColumn(_timeProvider); | ||
var task = new ProgressTask(1, "Test", 100) { Value = value }; | ||
task.Unsafe().StartTime = startTime; | ||
|
||
var result = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
|
||
result.GetText().Should().Be(expected); | ||
} | ||
|
||
[Fact] | ||
public void RenderWhenRemainingTimeExceeds99HoursShouldReturnAsterisks() | ||
{ | ||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
var currentTime = startTime.AddHours(1); | ||
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime)); | ||
|
||
var column = new LongRemainingTimeColumn(_timeProvider); | ||
var task = new ProgressTask(1, "Test", 10000) { Value = 1 }; | ||
task.Unsafe().StartTime = startTime; | ||
|
||
var result = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
|
||
result.GetText().Should().Be("**:**:**"); | ||
} | ||
|
||
[Fact] | ||
public void RenderEstimationBehaviorShouldBeCorrect() | ||
{ | ||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
var currentTime = startTime.AddMinutes(5); | ||
_timeProvider.GetUtcNow().Returns( | ||
new DateTimeOffset(currentTime), | ||
new DateTimeOffset(currentTime), | ||
new DateTimeOffset(currentTime.AddMinutes(20)), | ||
new DateTimeOffset(currentTime.AddMinutes(200))); | ||
|
||
var column = new LongRemainingTimeColumn(_timeProvider); | ||
var task = new ProgressTask(1, "Test", 100) { Value = 25 }; | ||
task.Unsafe().StartTime = startTime; | ||
|
||
// First render to set the estimated finish time | ||
var firstResult = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
|
||
// Second render with unchanged task value | ||
var secondResult = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
secondResult.GetText().Should().Be(firstResult.GetText()); | ||
|
||
// Third render after the estimated finish time | ||
var thirdResult = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
thirdResult.GetText().Should().Be("--:--:--"); | ||
|
||
// Update task value and render again | ||
task.Value = 50; | ||
var fourthResult = column.Render(_renderOptions, task, TimeSpan.Zero); | ||
fourthResult.GetText().Should().NotBe(secondResult.GetText()); | ||
} | ||
|
||
[Fact] | ||
public void RenderWithCustomAndNullStylesShouldUseCorrectStyle() | ||
{ | ||
var startTime = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc); | ||
var currentTime = startTime.AddMinutes(5); | ||
_timeProvider.GetUtcNow().Returns(new DateTimeOffset(currentTime)); | ||
|
||
var task = new ProgressTask(1, "Test", 100) { Value = 25 }; | ||
task.Unsafe().StartTime = startTime; | ||
|
||
// Custom style | ||
var customStyle = new Style(foreground: Color.Red); | ||
var columnWithCustomStyle = new LongRemainingTimeColumn(_timeProvider) { Style = customStyle }; | ||
var customStyleResult = columnWithCustomStyle.Render(_renderOptions, task, TimeSpan.Zero); | ||
customStyleResult.Should().NotBeNull(); | ||
customStyleResult.Render(_renderOptions, int.MaxValue).Should() | ||
.AllSatisfy(s => s.Style.Should().Be(customStyle)); | ||
|
||
// Null style | ||
var columnWithNullStyle = new LongRemainingTimeColumn(_timeProvider) { Style = null }; | ||
var nullStyleResult = columnWithNullStyle.Render(_renderOptions, task, TimeSpan.Zero); | ||
nullStyleResult.Should().NotBeNull(); | ||
nullStyleResult.Render(_renderOptions, int.MaxValue).Should() | ||
.AllSatisfy(s => s.Style.Should().Be(Style.Plain)); | ||
} | ||
} |
Oops, something went wrong.