added functionality to save/move tokenizer file

This commit is contained in:
PaulKoudelka committed 2026-04-10 18:31:20 +02:00
1 parent 09258c7548
commit 0854debc00
23 files changed
+839 -264

No files matched your search

@@ -34,7 +34,7 @@
</ChildContent>
<FooterContent>
<MudElement Style="flex: 0 0 auto;">
<MudTextField
<UserPromptComponent
T="string"
@ref="@this.inputField"
@bind-Text="@this.userInput"
@@ -50,8 +50,11 @@
Disabled="@this.IsInputForbidden()"
Immediate="@true"
OnKeyUp="@this.InputKeyEvent"
WhenTextChangedAsync="@(_ =>this.CalculateTokenCount())"
UserAttributes="@USER_INPUT_ATTRIBUTES"
Class="@this.UserInputClass"
DebounceTime="TimeSpan.FromSeconds(1)"
HelperText="@this.TokenCountMessage"
Style="@this.UserInputStyle"/>
</MudElement>
<MudToolBar WrapContent="true" Gutters="@false" Class="border border-solid rounded" Style="border-color: lightgrey; gap: 2px;">
@@ -3,6 +3,7 @@ using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@@ -44,6 +45,8 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject]
private IDialogService DialogService { get; init; } = null!;
[Inject]
private RustService RustService { get; init; } = null!;
[Inject]
private IJSRuntime JsRuntime { get; init; } = null!;
@@ -69,10 +72,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private Guid currentChatThreadId = Guid.Empty;
private CancellationTokenSource? cancellationTokenSource;
private HashSet<FileAttachment> chatDocumentPaths = [];
private string tokenCount = "0";
private string TokenCountMessage => $"{this.T("Estimated amount of tokens:")} {this.tokenCount}";
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
private MudTextField<string> inputField = null!;
private UserPromptComponent<string> inputField = null!;
#region Overrides of ComponentBase
@@ -460,6 +465,9 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Was a modifier key pressed as well?
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
if (isEnter)
await this.CalculateTokenCount();
// Depending on the user's settings, might react to shortcuts:
switch (this.SettingsManager.ConfigurationData.Chat.ShortcutSendBehavior)
{
@@ -596,6 +604,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.chatDocumentPaths.Clear();
await this.inputField.BlurAsync();
this.tokenCount = "0";
// Enable the stream state for the chat component:
this.isStreaming = true;
@@ -978,6 +987,25 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
return Task.CompletedTask;
}
private async Task CalculateTokenCount()
{
if (this.inputField.Value is null)
{
this.tokenCount = "0";
return;
}
var response = await this.RustService.GetTokenCount(this.inputField.Value);
if (response is null)
return;
if (!response.Value.Success)
{
this.Logger.LogWarning($"Failed to calculate token count: {response.Value.Message}");
return;
}
this.tokenCount = response.Value.TokenCount.ToString();
this.StateHasChanged();
}
#region Overrides of MSGComponentBase
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
@@ -5,12 +5,16 @@
T="string"
Text="@this.File"
Label="@this.Label"
ReadOnly="@true"
ReadOnly="@(!this.IsClearable)"
Validation="@this.Validation"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.AttachFile"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
Variant="Variant.Outlined"
Clearable="this.IsClearable"
Error="@this.Error"
ErrorText="@this.ErrorText"
OnClearButtonClick="@this.OnClear"
/>
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen" Variant="Variant.Outlined" Color="Color.Primary" Disabled="this.Disabled" OnClick="@this.OpenFileDialog">
@@ -2,6 +2,7 @@ using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
namespace AIStudio.Components;
@@ -27,7 +28,19 @@ public partial class SelectFile : MSGComponentBase
[Parameter]
public Func<string, string?> Validation { get; set; } = _ => null;
[Parameter]
public bool IsClearable { get; set; } = false;
[Parameter]
public bool Error { get; set; } = false;
[Parameter]
public string ErrorText { get; set; } = string.Empty;
[Parameter]
public Func<MouseEventArgs, Task> OnClear { get; set; } = _ => Task.CompletedTask;
[Inject]
public RustService RustService { get; set; } = null!;
@@ -52,7 +65,7 @@ public partial class SelectFile : MSGComponentBase
this.File = file;
this.FileChanged.InvokeAsync(file);
}
private async Task OpenFileDialog()
{
var response = await this.RustService.SelectFile(this.FileDialogTitle, this.Filter, string.IsNullOrWhiteSpace(this.File) ? null : this.File);
@@ -0,0 +1,68 @@
using Microsoft.AspNetCore.Components;
using Timer = System.Timers.Timer;
namespace AIStudio.Components;
/// <summary>
/// Debounced multi-line text input built on <see cref="MudTextField{T}"/>.
/// Keeps the base API while adding a debounce timer.
/// Callers can override any property as usual.
/// </summary>
public class UserPromptComponent<T> : MudTextField<T>
{
[Parameter]
public TimeSpan DebounceTime { get; set; } = TimeSpan.FromMilliseconds(800);
[Parameter]
public Func<string, Task> WhenTextChangedAsync { get; set; } = _ => Task.CompletedTask;
private readonly Timer debounceTimer = new();
private string text = string.Empty;
private string lastParameterText = string.Empty;
private string lastNotifiedText = string.Empty;
private bool isInitialized;
protected override async Task OnInitializedAsync()
{
this.text = this.Text ?? string.Empty;
this.lastParameterText = this.text;
this.lastNotifiedText = this.text;
this.debounceTimer.AutoReset = false;
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
this.debounceTimer.Elapsed += (_, _) =>
{
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;
await base.OnInitializedAsync();
}
protected override async Task OnParametersSetAsync()
{
// Ensure the timer uses the latest debouncing interval:
if (!this.isInitialized)
return;
if(Math.Abs(this.debounceTimer.Interval - this.DebounceTime.TotalMilliseconds) > 1)
this.debounceTimer.Interval = this.DebounceTime.TotalMilliseconds;
// Only sync when the parent's parameter actually changed since the last change:
if (this.Text != this.lastParameterText)
{
this.text = this.Text ?? string.Empty;
this.lastParameterText = this.text;
}
this.debounceTimer.Stop();
this.debounceTimer.Start();
await base.OnParametersSetAsync();
}
}