Skip to content

Add a writer mode #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/MindWork AI Studio/Components/MSGComponentBase.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
using AIStudio.Settings;

using Microsoft.AspNetCore.Components;

namespace AIStudio.Components;

public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{
[Inject]
protected SettingsManager SettingsManager { get; init; } = null!;

[Inject]
protected MessageBus MessageBus { get; init; } = null!;

Expand Down
1 change: 1 addition & 0 deletions app/MindWork AI Studio/Layout/MainLayout.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ protected override async Task OnInitializedAsync()
new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false),
new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),
Expand Down
3 changes: 0 additions & 3 deletions app/MindWork AI Studio/Pages/Chat.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ namespace AIStudio.Pages;
/// </summary>
public partial class Chat : MSGComponentBase, IAsyncDisposable
{
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;

[Inject]
private ThreadSafeRandom RNG { get; init; } = null!;

Expand Down
55 changes: 55 additions & 0 deletions app/MindWork AI Studio/Pages/Writer.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
@attribute [Route(Routes.WRITER)]
@inherits MSGComponentBase

<MudText Typo="Typo.h3" Class="mb-2 mr-3">
Writer
</MudText>

<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
<InnerScrolling HeaderHeight="12.3em">
<ChildContent>
<MudTextField
@ref="@this.textField"
T="string"
Label="Write your text"
@bind-Text="@this.userInput"
Immediate="@true"
Lines="16"
MaxLines="16"
Typo="Typo.body1"
Variant="Variant.Outlined"
InputMode="InputMode.text"
FullWidth="@true"
OnKeyDown="@this.InputKeyEvent"
UserAttributes="@USER_INPUT_ATTRIBUTES"/>

<MudTextField
T="string"
Label="Your stage directions"
@bind-Text="@this.userDirection"
Immediate="@true"
Lines="4"
MaxLines="4"
Typo="Typo.body1"
Variant="Variant.Outlined"
InputMode="InputMode.text"
FullWidth="@true"
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</ChildContent>
<FooterContent>
@if (this.isStreaming)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<MudTextField
T="string"
Label="Suggestion"
@bind-Text="@this.suggestion"
ReadOnly="@true"
Lines="3"
Typo="Typo.body1"
Variant="Variant.Outlined"
FullWidth="@true"
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
</FooterContent>
</InnerScrolling>
177 changes: 177 additions & 0 deletions app/MindWork AI Studio/Pages/Writer.razor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
using AIStudio.Chat;
using AIStudio.Components;
using AIStudio.Provider;

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!;

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;
private string userDirection = string.Empty;
private string suggestion = string.Empty;

#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

public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
return Task.CompletedTask;
}

public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
{
return Task.FromResult(default(TResult));
}

#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,
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;
this.chatThread.Blocks.Clear();
this.chatThread.Blocks.Add(new ContentBlock
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
// We use the maximum 160 characters from the end of the text:
Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
},
});

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();

await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
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
}
1 change: 1 addition & 0 deletions app/MindWork AI Studio/Routes.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public sealed partial class Routes
public const string ASSISTANTS = "/assistants";
public const string SETTINGS = "/settings";
public const string SUPPORTERS = "/supporters";
public const string WRITER = "/writer";

// ReSharper disable InconsistentNaming
public const string ASSISTANT_TRANSLATION = "/assistant/translation";
Expand Down