Skip to content

Commit 0a951ea

Browse files
Added the writer mode experiment (#167)
1 parent 432ae30 commit 0a951ea

File tree

6 files changed

+239
-3
lines changed

6 files changed

+239
-3
lines changed

app/MindWork AI Studio/Components/MSGComponentBase.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
using AIStudio.Settings;
2+
13
using Microsoft.AspNetCore.Components;
24

35
namespace AIStudio.Components;
46

57
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
68
{
9+
[Inject]
10+
protected SettingsManager SettingsManager { get; init; } = null!;
11+
712
[Inject]
813
protected MessageBus MessageBus { get; init; } = null!;
914

app/MindWork AI Studio/Layout/MainLayout.razor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ protected override async Task OnInitializedAsync()
103103
new("Home", Icons.Material.Filled.Home, palette.DarkLighten, palette.GrayLight, Routes.HOME, true),
104104
new("Chat", Icons.Material.Filled.Chat, palette.DarkLighten, palette.GrayLight, Routes.CHAT, false),
105105
new("Assistants", Icons.Material.Filled.Apps, palette.DarkLighten, palette.GrayLight, Routes.ASSISTANTS, false),
106+
new("Writer", Icons.Material.Filled.Create, palette.DarkLighten, palette.GrayLight, Routes.WRITER, false),
106107
new("Supporters", Icons.Material.Filled.Favorite, palette.Error.Value, "#801a00", Routes.SUPPORTERS, false),
107108
new("About", Icons.Material.Filled.Info, palette.DarkLighten, palette.GrayLight, Routes.ABOUT, false),
108109
new("Settings", Icons.Material.Filled.Settings, palette.DarkLighten, palette.GrayLight, Routes.SETTINGS, false),

app/MindWork AI Studio/Pages/Chat.razor.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ namespace AIStudio.Pages;
1717
/// </summary>
1818
public partial class Chat : MSGComponentBase, IAsyncDisposable
1919
{
20-
[Inject]
21-
private SettingsManager SettingsManager { get; init; } = null!;
22-
2320
[Inject]
2421
private ThreadSafeRandom RNG { get; init; } = null!;
2522

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
@attribute [Route(Routes.WRITER)]
2+
@inherits MSGComponentBase
3+
4+
<MudText Typo="Typo.h3" Class="mb-2 mr-3">
5+
Writer
6+
</MudText>
7+
8+
<ProviderSelection @bind-ProviderSettings="@this.providerSettings"/>
9+
<InnerScrolling HeaderHeight="12.3em">
10+
<ChildContent>
11+
<MudTextField
12+
@ref="@this.textField"
13+
T="string"
14+
Label="Write your text"
15+
@bind-Text="@this.userInput"
16+
Immediate="@true"
17+
Lines="16"
18+
MaxLines="16"
19+
Typo="Typo.body1"
20+
Variant="Variant.Outlined"
21+
InputMode="InputMode.text"
22+
FullWidth="@true"
23+
OnKeyDown="@this.InputKeyEvent"
24+
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
25+
26+
<MudTextField
27+
T="string"
28+
Label="Your stage directions"
29+
@bind-Text="@this.userDirection"
30+
Immediate="@true"
31+
Lines="4"
32+
MaxLines="4"
33+
Typo="Typo.body1"
34+
Variant="Variant.Outlined"
35+
InputMode="InputMode.text"
36+
FullWidth="@true"
37+
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
38+
</ChildContent>
39+
<FooterContent>
40+
@if (this.isStreaming)
41+
{
42+
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
43+
}
44+
<MudTextField
45+
T="string"
46+
Label="Suggestion"
47+
@bind-Text="@this.suggestion"
48+
ReadOnly="@true"
49+
Lines="3"
50+
Typo="Typo.body1"
51+
Variant="Variant.Outlined"
52+
FullWidth="@true"
53+
UserAttributes="@USER_INPUT_ATTRIBUTES"/>
54+
</FooterContent>
55+
</InnerScrolling>
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
using AIStudio.Chat;
2+
using AIStudio.Components;
3+
using AIStudio.Provider;
4+
5+
using Microsoft.AspNetCore.Components;
6+
using Microsoft.AspNetCore.Components.Web;
7+
8+
using Timer = System.Timers.Timer;
9+
10+
namespace AIStudio.Pages;
11+
12+
public partial class Writer : MSGComponentBase, IAsyncDisposable
13+
{
14+
[Inject]
15+
private ILogger<Chat> Logger { get; init; } = null!;
16+
17+
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
18+
private readonly Timer typeTimer = new(TimeSpan.FromMilliseconds(1_500));
19+
20+
private MudTextField<string> textField = null!;
21+
private AIStudio.Settings.Provider providerSettings;
22+
private ChatThread? chatThread;
23+
private bool isStreaming;
24+
private string userInput = string.Empty;
25+
private string userDirection = string.Empty;
26+
private string suggestion = string.Empty;
27+
28+
#region Overrides of ComponentBase
29+
30+
protected override async Task OnInitializedAsync()
31+
{
32+
this.ApplyFilters([], []);
33+
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
34+
this.typeTimer.Elapsed += async (_, _) => await this.InvokeAsync(this.GetSuggestions);
35+
this.typeTimer.AutoReset = false;
36+
37+
await base.OnInitializedAsync();
38+
}
39+
40+
#endregion
41+
42+
#region Overrides of MSGComponentBase
43+
44+
public override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
45+
{
46+
return Task.CompletedTask;
47+
}
48+
49+
public override Task<TResult?> ProcessMessageWithResult<TPayload, TResult>(ComponentBase? sendingComponent, Event triggeredEvent, TPayload? data) where TResult : default where TPayload : default
50+
{
51+
return Task.FromResult(default(TResult));
52+
}
53+
54+
#endregion
55+
56+
private bool IsProviderSelected => this.providerSettings.UsedLLMProvider != LLMProviders.NONE;
57+
58+
private async Task InputKeyEvent(KeyboardEventArgs keyEvent)
59+
{
60+
var key = keyEvent.Code.ToLowerInvariant();
61+
var isTab = key is "tab";
62+
var isModifier = keyEvent.AltKey || keyEvent.CtrlKey || keyEvent.MetaKey || keyEvent.ShiftKey;
63+
64+
if (isTab && !isModifier)
65+
{
66+
await this.textField.FocusAsync();
67+
this.AcceptNextWord();
68+
return;
69+
}
70+
71+
if (isTab && isModifier)
72+
{
73+
await this.textField.FocusAsync();
74+
this.AcceptEntireSuggestion();
75+
return;
76+
}
77+
78+
if(!isModifier)
79+
{
80+
this.typeTimer.Stop();
81+
this.typeTimer.Start();
82+
}
83+
}
84+
85+
private async Task GetSuggestions()
86+
{
87+
if (!this.IsProviderSelected)
88+
return;
89+
90+
this.chatThread ??= new()
91+
{
92+
WorkspaceId = Guid.Empty,
93+
ChatId = Guid.NewGuid(),
94+
Name = string.Empty,
95+
Seed = 798798,
96+
SystemPrompt = """
97+
You are an assistant who helps with writing documents. You receive a sample
98+
from a document as input. As output, you provide how the begun sentence could
99+
continue. You give exactly one variant, not multiple. If the current sentence
100+
is complete, you provide an empty response. You do not ask questions, and you
101+
do not repeat the task.
102+
""",
103+
Blocks = [],
104+
};
105+
106+
var time = DateTimeOffset.Now;
107+
this.chatThread.Blocks.Clear();
108+
this.chatThread.Blocks.Add(new ContentBlock
109+
{
110+
Time = time,
111+
ContentType = ContentType.TEXT,
112+
Role = ChatRole.USER,
113+
Content = new ContentText
114+
{
115+
// We use the maximum 160 characters from the end of the text:
116+
Text = this.userInput.Length > 160 ? this.userInput[^160..] : this.userInput,
117+
},
118+
});
119+
120+
var aiText = new ContentText
121+
{
122+
// We have to wait for the remote
123+
// for the content stream:
124+
InitialRemoteWait = true,
125+
};
126+
127+
this.chatThread?.Blocks.Add(new ContentBlock
128+
{
129+
Time = time,
130+
ContentType = ContentType.TEXT,
131+
Role = ChatRole.AI,
132+
Content = aiText,
133+
});
134+
135+
this.isStreaming = true;
136+
this.StateHasChanged();
137+
138+
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.SettingsManager, this.providerSettings.Model, this.chatThread);
139+
this.suggestion = aiText.Text;
140+
141+
this.isStreaming = false;
142+
this.StateHasChanged();
143+
}
144+
145+
private void AcceptEntireSuggestion()
146+
{
147+
if(this.userInput.Last() != ' ')
148+
this.userInput += ' ';
149+
150+
this.userInput += this.suggestion;
151+
this.suggestion = string.Empty;
152+
this.StateHasChanged();
153+
}
154+
155+
private void AcceptNextWord()
156+
{
157+
var words = this.suggestion.Split(' ', StringSplitOptions.RemoveEmptyEntries);
158+
if(words.Length == 0)
159+
return;
160+
161+
if(this.userInput.Last() != ' ')
162+
this.userInput += ' ';
163+
164+
this.userInput += words[0] + ' ';
165+
this.suggestion = string.Join(' ', words.Skip(1));
166+
this.StateHasChanged();
167+
}
168+
169+
#region Implementation of IAsyncDisposable
170+
171+
public ValueTask DisposeAsync()
172+
{
173+
return ValueTask.CompletedTask;
174+
}
175+
176+
#endregion
177+
}

app/MindWork AI Studio/Routes.razor.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public sealed partial class Routes
88
public const string ASSISTANTS = "/assistants";
99
public const string SETTINGS = "/settings";
1010
public const string SUPPORTERS = "/supporters";
11+
public const string WRITER = "/writer";
1112

1213
// ReSharper disable InconsistentNaming
1314
public const string ASSISTANT_TRANSLATION = "/assistant/translation";

0 commit comments

Comments
 (0)