AI-Studio/app/MindWork AI Studio/Pages/Writer.razor.cs
Thorsten Sommer 3e6e3bdcbd
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions
Fixed error messages for provider requests (#778)
2026-05-25 17:32:54 +02:00

186 lines
5.7 KiB
C#

using AIStudio.Chat;
using AIStudio.Components;
using AIStudio.Provider;
using Microsoft.AspNetCore.Components.Web;
using Timer = System.Timers.Timer;
namespace AIStudio.Pages;
public partial class Writer : MSGComponentBase
{
private static readonly ILogger<Writer> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Writer>();
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
private MudTextField<string> textField = null!;
private AIStudio.Settings.Provider providerSettings = AIStudio.Settings.Provider.NONE;
private ChatThread? chatThread;
private bool isStreaming;
private string userInput = string.Empty;
private string userDirection = string.Empty;
private string suggestion = string.Empty;
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
this.typeTimer.Elapsed += async (_, _) => await this.InvokeAsync(this.GetSuggestions);
this.typeTimer.AutoReset = false;
await base.OnInitializedAsync();
}
#endregion
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
{
var key = keyEvent.Code.ToLowerInvariant();
var isTab = key is "tab";
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
if (isTab && !isModifier)
{
await this.textField.FocusAsync();
this.AcceptNextWord();
return;
}
if (isTab && isModifier)
{
await this.textField.FocusAsync();
this.AcceptEntireSuggestion();
return;
}
if(!isModifier)
{
this.typeTimer.Stop();
this.typeTimer.Start();
}
}
private async Task GetSuggestions()
{
if (!this.IsProviderSelected)
return;
this.chatThread ??= new()
{
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
Name = string.Empty,
SystemPrompt = """
You are an assistant who helps with writing documents. You receive a sample
from a document as input. As output, you provide how the begun sentence could
continue. You give exactly one variant, not multiple. If the current sentence
is complete, you provide an empty response. You do not ask questions, and you
do not repeat the task.
""",
Blocks = [],
};
var time = DateTimeOffset.Now;
var lastUserPrompt = new ContentText
{
// We use the maximum 160 characters from the end of the text:
Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
};
this.chatThread.Blocks.Clear();
this.chatThread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = lastUserPrompt,
});
var aiText = new ContentText
{
// We have to wait for the remote
// for the content stream:
InitialRemoteWait = true,
};
var aiBlock = new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.AI,
Content = aiText,
};
this.chatThread?.Blocks.Add(aiBlock);
this.isStreaming = true;
this.StateHasChanged();
try
{
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.providerSettings.Model, lastUserPrompt, this.chatThread);
this.suggestion = aiText.Text;
}
catch (ProviderRequestException e)
{
LOGGER.LogError(e, "The provider request failed for writer suggestions. Status={StatusCode}, Reason='{ReasonPhrase}', Body='{ResponseBody}'", e.StatusCode, e.ReasonPhrase, e.ResponseBody);
await this.MessageBus.SendError(new(Icons.Material.Filled.CloudOff, e.UserMessage));
this.suggestion = string.Empty;
if (string.IsNullOrWhiteSpace(aiText.Text))
this.chatThread?.Blocks.Remove(aiBlock);
}
finally
{
this.isStreaming = false;
this.StateHasChanged();
}
}
private void AcceptEntireSuggestion()
{
if(this.userInput.Last() != ' ')
this.userInput += ' ';
this.userInput += this.suggestion;
this.suggestion = string.Empty;
this.StateHasChanged();
}
private void AcceptNextWord()
{
var words = this.suggestion.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if(words.Length == 0)
return;
if(this.userInput.Last() != ' ')
this.userInput += ' ';
this.userInput += words[0] + ' ';
this.suggestion = string.Join(' ', words.Skip(1));
this.StateHasChanged();
}
#region Overrides of MSGComponentBase
protected override void DisposeResources()
{
try
{
this.typeTimer.Stop();
this.typeTimer.Dispose();
}
catch
{
// ignore
}
base.DisposeResources();
}
#endregion
}