Skip to content

Commit 05a7e44

Browse files
Add a grammar and spell checker assistant (#72)
1 parent 77e7e04 commit 05a7e44

File tree

14 files changed

+211
-4
lines changed

14 files changed

+211
-4
lines changed

app/MindWork AI Studio/Components/App.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
1414
<link href="app.css" rel="stylesheet" />
1515
<HeadOutlet/>
16+
<script src="diff.js"></script>
1617
</head>
1718

1819
<body style="overflow: hidden;">

app/MindWork AI Studio/Components/AssistantBase.razor

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,44 @@
1616
}
1717
</MudForm>
1818
<Issues IssuesData="@this.inputIssues"/>
19+
20+
@if (this.isProcessing)
21+
{
22+
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
23+
}
24+
<div id="@ASSISTANT_RESULT_DIV_ID" class="mr-2 mt-3">
25+
@if (this.ShowResult && this.resultingContentBlock is not null)
26+
{
27+
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content"/>
28+
}
29+
</div>
30+
31+
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
32+
</div>
1933

20-
@if (this.resultingContentBlock is not null)
34+
@if (this.FooterButtons.Count > 0)
2135
{
22-
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
36+
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="mt-3 mr-2">
37+
@foreach (var buttonData in this.FooterButtons)
38+
{
39+
switch (buttonData)
40+
{
41+
case var _ when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
42+
<MudTooltip Text="@buttonData.Tooltip">
43+
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
44+
@buttonData.Text
45+
</MudButton>
46+
</MudTooltip>
47+
break;
48+
49+
default:
50+
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
51+
@buttonData.Text
52+
</MudButton>
53+
break;
54+
}
55+
}
56+
</MudStack>
2357
}
2458
</ChildContent>
2559
</InnerScrolling>

app/MindWork AI Studio/Components/AssistantBase.razor.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ public abstract partial class AssistantBase : ComponentBase
1818
[Inject]
1919
protected ThreadSafeRandom RNG { get; init; } = null!;
2020

21+
[Inject]
22+
protected ISnackbar Snackbar { get; init; } = null!;
23+
24+
[Inject]
25+
protected Rust Rust { get; init; } = null!;
26+
27+
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
28+
internal const string ASSISTANT_RESULT_DIV_ID = "assistantResult";
29+
2130
protected abstract string Title { get; }
2231

2332
protected abstract string Description { get; }
@@ -26,6 +35,10 @@ public abstract partial class AssistantBase : ComponentBase
2635

2736
private protected virtual RenderFragment? Body => null;
2837

38+
protected virtual bool ShowResult => true;
39+
40+
protected virtual IReadOnlyList<ButtonData> FooterButtons => [];
41+
2942
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
3043

3144
protected AIStudio.Settings.Provider providerSettings;
@@ -35,6 +48,7 @@ public abstract partial class AssistantBase : ComponentBase
3548
private ChatThread? chatThread;
3649
private ContentBlock? resultingContentBlock;
3750
private string[] inputIssues = [];
51+
private bool isProcessing;
3852

3953
#region Overrides of ComponentBase
4054

@@ -96,7 +110,7 @@ protected DateTimeOffset AddUserRequest(string request)
96110
return time;
97111
}
98112

99-
protected async Task AddAIResponseAsync(DateTimeOffset time)
113+
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
100114
{
101115
var aiText = new ContentText
102116
{
@@ -114,10 +128,26 @@ protected async Task AddAIResponseAsync(DateTimeOffset time)
114128
};
115129

116130
this.chatThread?.Blocks.Add(this.resultingContentBlock);
131+
this.isProcessing = true;
132+
this.StateHasChanged();
117133

118134
// Use the selected provider to get the AI response.
119135
// By awaiting this line, we wait for the entire
120136
// content to be streamed.
121137
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
138+
139+
this.isProcessing = false;
140+
this.StateHasChanged();
141+
142+
// Return the AI response:
143+
return aiText.Text;
144+
}
145+
146+
private static string? GetButtonIcon(string icon)
147+
{
148+
if(string.IsNullOrWhiteSpace(icon))
149+
return null;
150+
151+
return icon;
122152
}
123153
}

app/MindWork AI Studio/Components/AssistantBaseCore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
namespace AIStudio.Components;
55

66
//
7-
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
7+
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
88
//
99

1010
public abstract class AssistantBaseCore : AssistantBase

app/MindWork AI Studio/Components/Pages/About.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<ThirdPartyComponent Name="flexi_logger" Developer="emabee & Open Source Community" LicenseName="MIT & Apache-2.0" LicenseUrl="https://github.com/emabee/flexi_logger" RepositoryUrl="https://github.com/emabee/flexi_logger" UseCase="This Rust library is used to output the app's messages to the terminal. This is helpful during development and troubleshooting. This feature is initially invisible; when the app is started via the terminal, the messages become visible."/>
5050
<ThirdPartyComponent Name="HtmlAgilityPack" Developer="ZZZ Projects & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/zzzprojects/html-agility-pack/blob/master/LICENSE" RepositoryUrl="https://github.com/zzzprojects/html-agility-pack" UseCase="We use the HtmlAgilityPack to extract content from the web. This is necessary, e.g., when you provide a URL as input for an assistant."/>
5151
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant."/>
52+
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant."/>
5253
</MudGrid>
5354
</ExpansionPanel>
5455
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">

app/MindWork AI Studio/Components/Pages/Assistants.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
1313
<AssistantBlock Name="Text Summarizer" Description="Using a LLM to summarize a given text." Icon="@Icons.Material.Filled.TextSnippet" Link="/assistant/summarizer"/>
1414
<AssistantBlock Name="Translation" Description="Translate text into another language." Icon="@Icons.Material.Filled.Translate" Link="/assistant/translation"/>
15+
<AssistantBlock Name="Grammar & Spelling" Description="Check grammar and spelling of a given text." Icon="@Icons.Material.Filled.Edit" Link="/assistant/grammar-spelling"/>
1516
</MudStack>
1617

1718
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@using AIStudio.Tools
2+
@page "/assistant/grammar-spelling"
3+
@inherits AssistantBaseCore
4+
5+
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidateText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="Your input to check" Variant="Variant.Outlined" Lines="6" AutoGrow="@true" MaxLines="12" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
6+
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.NameSelectingOptional())" @bind-Value="@this.selectedTargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="Language" AllowOther="@true" OtherValue="CommonLanguages.OTHER" @bind-OtherInput="@this.customTargetLanguage" ValidateOther="@this.ValidateCustomLanguage" LabelOther="Custom language" />
7+
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
8+
9+
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.ProofreadText()">
10+
Proofread
11+
</MudButton>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using AIStudio.Tools;
2+
3+
namespace AIStudio.Components.Pages.GrammarSpelling;
4+
5+
public partial class AssistantGrammarSpelling : AssistantBaseCore
6+
{
7+
protected override string Title => "Grammar and Spelling Checker";
8+
9+
protected override string Description =>
10+
"""
11+
Check the grammar and spelling of a text.
12+
""";
13+
14+
protected override string SystemPrompt =>
15+
$"""
16+
You are an expert in languages and their rules. For example, you know how US and UK English or German in
17+
Germany and German in Austria differ. You receive text as input. You check the spelling and grammar of
18+
this text according to the rules of {this.SystemPromptLanguage()}. You never add information. You
19+
never ask the user for additional information. You do not attempt to improve the wording of the text.
20+
Your response includes only the corrected text. Do not explain your changes. If no changes are needed,
21+
you return the text unchanged.
22+
""";
23+
24+
protected override bool ShowResult => false;
25+
26+
protected override IReadOnlyList<ButtonData> FooterButtons => new[]
27+
{
28+
new ButtonData("Copy corrected text", Icons.Material.Filled.ContentCopy, Color.Default, string.Empty, this.CopyToClipboard),
29+
};
30+
31+
private string inputText = string.Empty;
32+
private CommonLanguages selectedTargetLanguage;
33+
private string customTargetLanguage = string.Empty;
34+
private string correctedText = string.Empty;
35+
36+
private string? ValidateText(string text)
37+
{
38+
if(string.IsNullOrWhiteSpace(text))
39+
return "Please provide a text as input. You might copy the desired text from a document or a website.";
40+
41+
return null;
42+
}
43+
44+
private string? ValidateCustomLanguage(string language)
45+
{
46+
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
47+
return "Please provide a custom language.";
48+
49+
return null;
50+
}
51+
52+
private string SystemPromptLanguage()
53+
{
54+
var lang = this.selectedTargetLanguage switch
55+
{
56+
CommonLanguages.AS_IS => "the source language",
57+
CommonLanguages.OTHER => this.customTargetLanguage,
58+
59+
_ => $"{this.selectedTargetLanguage.Name()}",
60+
};
61+
62+
if (string.IsNullOrWhiteSpace(lang))
63+
return "the source language";
64+
65+
return lang;
66+
}
67+
68+
private async Task ProofreadText()
69+
{
70+
if (!this.inputIsValid)
71+
return;
72+
73+
this.CreateChatThread();
74+
var time = this.AddUserRequest(this.inputText);
75+
76+
this.correctedText = await this.AddAIResponseAsync(time);
77+
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText);
78+
}
79+
80+
private async Task CopyToClipboard()
81+
{
82+
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, this.correctedText);
83+
}
84+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace AIStudio.Tools;
2+
3+
public readonly record struct ButtonData(string Text, string Icon, Color Color, string Tooltip, Func<Task> AsyncAction);

app/MindWork AI Studio/Tools/CommonLanguageExtensions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,12 @@ public static string NameSelecting(this CommonLanguages language)
4242

4343
return language.Name();
4444
}
45+
46+
public static string NameSelectingOptional(this CommonLanguages language)
47+
{
48+
if(language is CommonLanguages.AS_IS)
49+
return "Do not specify the language";
50+
51+
return language.Name();
52+
}
4553
}

0 commit comments

Comments
 (0)