2025-03-29 17:55:30 +00:00
|
|
|
using System.Text;
|
|
|
|
|
2024-12-03 14:32:38 +00:00
|
|
|
using AIStudio.Chat;
|
|
|
|
using AIStudio.Components;
|
|
|
|
using AIStudio.Provider;
|
2025-03-29 17:55:30 +00:00
|
|
|
using AIStudio.Tools.Services;
|
2024-12-03 14:32:38 +00:00
|
|
|
|
|
|
|
using Microsoft.AspNetCore.Components;
|
|
|
|
using Microsoft.AspNetCore.Components.Web;
|
|
|
|
|
|
|
|
using Timer = System.Timers.Timer;
|
|
|
|
|
|
|
|
namespace AIStudio.Pages;
|
|
|
|
|
|
|
|
public partial class Writer : MSGComponentBase, IAsyncDisposable
|
|
|
|
{
|
|
|
|
[Inject]
|
|
|
|
private ILogger<Chat> Logger { get; init; } = null!;
|
|
|
|
|
2025-03-29 17:55:30 +00:00
|
|
|
[Inject]
|
|
|
|
private RustService RustService { get; init; } = null!;
|
|
|
|
|
2024-12-03 14:32:38 +00:00
|
|
|
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;
|
|
|
|
private ChatThread? chatThread;
|
|
|
|
private bool isStreaming;
|
|
|
|
private string userInput = string.Empty;
|
2025-03-29 20:07:17 +00:00
|
|
|
private List<WriterChunk> chunks = new();
|
2024-12-03 14:32:38 +00:00
|
|
|
private string userDirection = string.Empty;
|
|
|
|
private string suggestion = string.Empty;
|
2025-04-12 19:13:33 +00:00
|
|
|
|
2024-12-03 14:32:38 +00:00
|
|
|
#region Overrides of ComponentBase
|
|
|
|
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
|
|
{
|
|
|
|
this.ApplyFilters([], []);
|
|
|
|
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
|
|
|
this.typeTimer.Elapsed += async (_, _) => await this.InvokeAsync(this.GetSuggestions);
|
|
|
|
this.typeTimer.AutoReset = false;
|
|
|
|
|
|
|
|
await base.OnInitializedAsync();
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
#region Overrides of MSGComponentBase
|
|
|
|
|
2025-01-21 14:36:22 +00:00
|
|
|
public override string ComponentName => nameof(Writer);
|
|
|
|
|
2024-12-03 14:32:38 +00:00
|
|
|
#endregion
|
|
|
|
|
|
|
|
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
|
2025-03-29 17:55:30 +00:00
|
|
|
|
|
|
|
private async Task LoadTextFile()
|
|
|
|
{
|
|
|
|
var result = await this.RustService.SelectFile("Load a text file");
|
|
|
|
if(result.UserCancelled)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if(!File.Exists(result.SelectedFilePath))
|
|
|
|
return;
|
|
|
|
|
|
|
|
var text = await File.ReadAllTextAsync(result.SelectedFilePath, Encoding.UTF8);
|
|
|
|
this.userInput = text;
|
2025-03-29 20:07:17 +00:00
|
|
|
this.ChunkText();
|
|
|
|
}
|
|
|
|
|
|
|
|
private void ChunkText()
|
|
|
|
{
|
|
|
|
this.chunks.Clear();
|
|
|
|
var startIndex = 0;
|
|
|
|
var contentSpan = this.userInput.AsSpan();
|
|
|
|
for (var index = 0; index < contentSpan.Length; index++)
|
|
|
|
{
|
|
|
|
if (contentSpan[index] is '\n')
|
|
|
|
{
|
|
|
|
var endIndex = index;
|
|
|
|
var lineMemory = this.userInput.AsMemory(startIndex..endIndex);
|
|
|
|
this.chunks.Add(new WriterChunk(lineMemory, false, false));
|
|
|
|
startIndex = endIndex + 1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.StateHasChanged();
|
2025-03-29 17:55:30 +00:00
|
|
|
}
|
2024-12-03 14:32:38 +00:00
|
|
|
|
|
|
|
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,
|
|
|
|
Seed = 798798,
|
|
|
|
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;
|
2025-02-10 17:49:22 +00:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
|
2024-12-03 14:32:38 +00:00
|
|
|
this.chatThread.Blocks.Clear();
|
|
|
|
this.chatThread.Blocks.Add(new ContentBlock
|
|
|
|
{
|
|
|
|
Time = time,
|
|
|
|
ContentType = ContentType.TEXT,
|
|
|
|
Role = ChatRole.USER,
|
2025-02-10 17:49:22 +00:00
|
|
|
Content = lastUserPrompt,
|
2024-12-03 14:32:38 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
var aiText = new ContentText
|
|
|
|
{
|
|
|
|
// We have to wait for the remote
|
|
|
|
// for the content stream:
|
|
|
|
InitialRemoteWait = true,
|
|
|
|
};
|
|
|
|
|
|
|
|
this.chatThread?.Blocks.Add(new ContentBlock
|
|
|
|
{
|
|
|
|
Time = time,
|
|
|
|
ContentType = ContentType.TEXT,
|
|
|
|
Role = ChatRole.AI,
|
|
|
|
Content = aiText,
|
|
|
|
});
|
|
|
|
|
|
|
|
this.isStreaming = true;
|
|
|
|
this.StateHasChanged();
|
|
|
|
|
2025-03-08 12:56:38 +00:00
|
|
|
this.chatThread = await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.providerSettings.Model, lastUserPrompt, this.chatThread);
|
2024-12-03 14:32:38 +00:00
|
|
|
this.suggestion = aiText.Text;
|
|
|
|
|
|
|
|
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 Implementation of IAsyncDisposable
|
|
|
|
|
|
|
|
public ValueTask DisposeAsync()
|
|
|
|
{
|
|
|
|
return ValueTask.CompletedTask;
|
|
|
|
}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
}
|