mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-18 08:37:59 +00:00
Fix the lifecycle of the debounced user prompt component
This commit is contained in:
2 files changed
+61
-16
No files matched your search
@@ -93,7 +93,6 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
private Guid loadedParameterWorkspaceId = Guid.Empty;
|
private Guid loadedParameterWorkspaceId = Guid.Empty;
|
||||||
private Guid foregroundChatId = Guid.Empty;
|
private Guid foregroundChatId = Guid.Empty;
|
||||||
private int workspaceHeaderSyncVersion;
|
private int workspaceHeaderSyncVersion;
|
||||||
private HashSet<FileAttachment> chatDocumentPaths = [];
|
|
||||||
private string tokenCount = "0";
|
private string tokenCount = "0";
|
||||||
private bool HasCustomTokenizer => !string.IsNullOrWhiteSpace(this.Provider.TokenizerPath);
|
private bool HasCustomTokenizer => !string.IsNullOrWhiteSpace(this.Provider.TokenizerPath);
|
||||||
private string TokenCountMessage => this.HasCustomTokenizer
|
private string TokenCountMessage => this.HasCustomTokenizer
|
||||||
@@ -394,7 +393,7 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
|
|
||||||
await this.ApplyLoadedChatParameterAsync();
|
await this.ApplyLoadedChatParameterAsync();
|
||||||
await this.SyncForegroundChatAsync();
|
await this.SyncForegroundChatAsync();
|
||||||
if (providerChanged && this.HasCustomTokenizer && this.inputField is not null)
|
if (providerChanged && this.HasCustomTokenizer)
|
||||||
await this.CalculateTokenCount();
|
await this.CalculateTokenCount();
|
||||||
|
|
||||||
await this.ConsumeMediaOutcomeAsync();
|
await this.ConsumeMediaOutcomeAsync();
|
||||||
@@ -1323,13 +1322,19 @@ public partial class ChatComponent : MSGComponentBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.inputField.Value is null)
|
//
|
||||||
|
// Read the text from the bound property rather than from the input field: the field is a
|
||||||
|
// component reference, which is only set once the component has rendered. Counting is also
|
||||||
|
// triggered while parameters are set, which happens before that.
|
||||||
|
//
|
||||||
|
var currentInput = this.UserInput;
|
||||||
|
if (string.IsNullOrEmpty(currentInput))
|
||||||
{
|
{
|
||||||
this.tokenCount = "0";
|
this.tokenCount = "0";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var response = await this.RustService.GetTokenCount(this.Provider, this.inputField.Value);
|
var response = await this.RustService.GetTokenCount(this.Provider, currentInput);
|
||||||
if (response is null)
|
if (response is null)
|
||||||
return;
|
return;
|
||||||
if (!response.Value.Success)
|
if (!response.Value.Success)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace AIStudio.Components;
|
|||||||
/// Keeps the base API while adding a debounce timer.
|
/// Keeps the base API while adding a debounce timer.
|
||||||
/// Callers can override any property as usual.
|
/// Callers can override any property as usual.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class UserPromptComponent<T> : MudTextField<T>
|
public class UserPromptComponent<T> : MudTextField<T>, IDisposable
|
||||||
{
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(800);
|
public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(800);
|
||||||
@@ -21,6 +21,7 @@ public class UserPromptComponent<T> : MudTextField<T>
|
|||||||
private string lastParameterText = string.Empty;
|
private string lastParameterText = string.Empty;
|
||||||
private string lastNotifiedText = string.Empty;
|
private string lastNotifiedText = string.Empty;
|
||||||
private bool isInitialized;
|
private bool isInitialized;
|
||||||
|
private bool isDisposed;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -29,16 +30,7 @@ public class UserPromptComponent<T> : MudTextField<T>
|
|||||||
this.lastNotifiedText = this.text;
|
this.lastNotifiedText = this.text;
|
||||||
this.debounceTimer.AutoReset = false;
|
this.debounceTimer.AutoReset = false;
|
||||||
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
|
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
|
||||||
this.debounceTimer.Elapsed += (_, _) =>
|
this.debounceTimer.Elapsed += this.WhenDebounceElapsed;
|
||||||
{
|
|
||||||
this.debounceTimer.Stop();
|
|
||||||
if (this.text == this.lastNotifiedText)
|
|
||||||
return;
|
|
||||||
|
|
||||||
this.lastNotifiedText = this.text;
|
|
||||||
this.InvokeAsync(async () => await this.TextChanged.InvokeAsync(this.text));
|
|
||||||
this.InvokeAsync(async () => await this.WhenTextChangedAsync(this.text));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
@@ -47,8 +39,11 @@ public class UserPromptComponent<T> : MudTextField<T>
|
|||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
// Ensure the timer uses the latest debouncing interval:
|
// Ensure the timer uses the latest debouncing interval:
|
||||||
if (!this.isInitialized)
|
if (!this.isInitialized || this.isDisposed)
|
||||||
|
{
|
||||||
|
await base.OnParametersSetAsync();
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1)
|
if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1)
|
||||||
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
|
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
|
||||||
@@ -65,4 +60,49 @@ public class UserPromptComponent<T> : MudTextField<T>
|
|||||||
|
|
||||||
await base.OnParametersSetAsync();
|
await base.OnParametersSetAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void WhenDebounceElapsed(object? sender, System.Timers.ElapsedEventArgs args)
|
||||||
|
{
|
||||||
|
this.debounceTimer.Stop();
|
||||||
|
|
||||||
|
//
|
||||||
|
// The timer runs on its own thread and may still fire while this component is being torn
|
||||||
|
// down. Notifying a renderer which is already gone would throw on that thread, where no
|
||||||
|
// caller is left to handle it.
|
||||||
|
//
|
||||||
|
if (this.isDisposed || this.text == this.lastNotifiedText)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.lastNotifiedText = this.text;
|
||||||
|
this.InvokeAsync(async () => await this.TextChanged.InvokeAsync(this.text)).Observe($"{nameof(UserPromptComponent<T>)}: notifying about changed text");
|
||||||
|
this.InvokeAsync(async () => await this.WhenTextChangedAsync(this.text)).Observe($"{nameof(UserPromptComponent<T>)}: handling changed text asynchronously");
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IDisposable
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (this.isDisposed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//
|
||||||
|
// Set before stopping the timer: the handler might be running on the timer thread right
|
||||||
|
// now, and this is what tells it to leave the gone renderer alone.
|
||||||
|
//
|
||||||
|
this.isDisposed = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.debounceTimer.Elapsed -= this.WhenDebounceElapsed;
|
||||||
|
this.debounceTimer.Stop();
|
||||||
|
this.debounceTimer.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user