Add a grammar and spell checker assistant (#72)

This commit is contained in:
Thorsten Sommer 2024-08-13 08:57:58 +02:00 committed by GitHub
parent 77e7e044e3
commit 05a7e44de4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 212 additions and 5 deletions

View File

@ -13,6 +13,7 @@
<link href="system/MudBlazor.Markdown/MudBlazor.Markdown.min.css" rel="stylesheet" />
<link href="app.css" rel="stylesheet" />
<HeadOutlet/>
<script src="diff.js"></script>
</head>
<body style="overflow: hidden;">

View File

@ -17,9 +17,43 @@
</MudForm>
<Issues IssuesData="@this.inputIssues"/>
@if (this.resultingContentBlock is not null)
@if (this.isProcessing)
{
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content" Class="mr-2"/>
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-6" />
}
<div id="@ASSISTANT_RESULT_DIV_ID" class="mr-2 mt-3">
@if (this.ShowResult && this.resultingContentBlock is not null)
{
<ContentBlockComponent Role="@this.resultingContentBlock.Role" Type="@this.resultingContentBlock.ContentType" Time="@this.resultingContentBlock.Time" Content="@this.resultingContentBlock.Content"/>
}
</div>
<div id="@AFTER_RESULT_DIV_ID" class="mt-3">
</div>
@if (this.FooterButtons.Count > 0)
{
<MudStack Row="@true" Wrap="Wrap.Wrap" Class="mt-3 mr-2">
@foreach (var buttonData in this.FooterButtons)
{
switch (buttonData)
{
case var _ when !string.IsNullOrWhiteSpace(buttonData.Tooltip):
<MudTooltip Text="@buttonData.Tooltip">
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
</MudTooltip>
break;
default:
<MudButton Variant="Variant.Filled" Color="@buttonData.Color" StartIcon="@GetButtonIcon(buttonData.Icon)" OnClick="async () => await buttonData.AsyncAction()">
@buttonData.Text
</MudButton>
break;
}
}
</MudStack>
}
</ChildContent>
</InnerScrolling>

View File

@ -18,6 +18,15 @@ public abstract partial class AssistantBase : ComponentBase
[Inject]
protected ThreadSafeRandom RNG { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
[Inject]
protected Rust Rust { get; init; } = null!;
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
internal const string ASSISTANT_RESULT_DIV_ID = "assistantResult";
protected abstract string Title { get; }
protected abstract string Description { get; }
@ -26,6 +35,10 @@ public abstract partial class AssistantBase : ComponentBase
private protected virtual RenderFragment? Body => null;
protected virtual bool ShowResult => true;
protected virtual IReadOnlyList<ButtonData> FooterButtons => [];
protected static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
protected AIStudio.Settings.Provider providerSettings;
@ -35,6 +48,7 @@ public abstract partial class AssistantBase : ComponentBase
private ChatThread? chatThread;
private ContentBlock? resultingContentBlock;
private string[] inputIssues = [];
private bool isProcessing;
#region Overrides of ComponentBase
@ -96,7 +110,7 @@ public abstract partial class AssistantBase : ComponentBase
return time;
}
protected async Task AddAIResponseAsync(DateTimeOffset time)
protected async Task<string> AddAIResponseAsync(DateTimeOffset time)
{
var aiText = new ContentText
{
@ -114,10 +128,26 @@ public abstract partial class AssistantBase : ComponentBase
};
this.chatThread?.Blocks.Add(this.resultingContentBlock);
this.isProcessing = true;
this.StateHasChanged();
// Use the selected provider to get the AI response.
// By awaiting this line, we wait for the entire
// content to be streamed.
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
this.isProcessing = false;
this.StateHasChanged();
// Return the AI response:
return aiText.Text;
}
private static string? GetButtonIcon(string icon)
{
if(string.IsNullOrWhiteSpace(icon))
return null;
return icon;
}
}

View File

@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Components.Rendering;
namespace AIStudio.Components;
//
// See https://stackoverflow.com/a/77300384/2258393 for why this class is needed
// See https://stackoverflow.com/a/77300384/2258393 for why this class is necessary
//
public abstract class AssistantBaseCore : AssistantBase

View File

@ -49,6 +49,7 @@
<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."/>
<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."/>
<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."/>
<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."/>
</MudGrid>
</ExpansionPanel>
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">

View File

@ -12,6 +12,7 @@
<MudStack Row="@true" Wrap="@Wrap.Wrap" Class="mb-3">
<AssistantBlock Name="Text Summarizer" Description="Using a LLM to summarize a given text." Icon="@Icons.Material.Filled.TextSnippet" Link="/assistant/summarizer"/>
<AssistantBlock Name="Translation" Description="Translate text into another language." Icon="@Icons.Material.Filled.Translate" Link="/assistant/translation"/>
<AssistantBlock Name="Grammar & Spelling" Description="Check grammar and spelling of a given text." Icon="@Icons.Material.Filled.Edit" Link="/assistant/grammar-spelling"/>
</MudStack>
<MudText Typo="Typo.h4" Class="mb-2 mr-3 mt-6">

View File

@ -0,0 +1,11 @@
@using AIStudio.Tools
@page "/assistant/grammar-spelling"
@inherits AssistantBaseCore
<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"/>
<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" />
<ProviderSelection @bind-ProviderSettings="@this.providerSettings" ValidateProvider="@this.ValidatingProvider"/>
<MudButton Variant="Variant.Filled" Class="mb-3" OnClick="() => this.ProofreadText()">
Proofread
</MudButton>

View File

@ -0,0 +1,84 @@
using AIStudio.Tools;
namespace AIStudio.Components.Pages.GrammarSpelling;
public partial class AssistantGrammarSpelling : AssistantBaseCore
{
protected override string Title => "Grammar and Spelling Checker";
protected override string Description =>
"""
Check the grammar and spelling of a text.
""";
protected override string SystemPrompt =>
$"""
You are an expert in languages and their rules. For example, you know how US and UK English or German in
Germany and German in Austria differ. You receive text as input. You check the spelling and grammar of
this text according to the rules of {this.SystemPromptLanguage()}. You never add information. You
never ask the user for additional information. You do not attempt to improve the wording of the text.
Your response includes only the corrected text. Do not explain your changes. If no changes are needed,
you return the text unchanged.
""";
protected override bool ShowResult => false;
protected override IReadOnlyList<ButtonData> FooterButtons => new[]
{
new ButtonData("Copy corrected text", Icons.Material.Filled.ContentCopy, Color.Default, string.Empty, this.CopyToClipboard),
};
private string inputText = string.Empty;
private CommonLanguages selectedTargetLanguage;
private string customTargetLanguage = string.Empty;
private string correctedText = string.Empty;
private string? ValidateText(string text)
{
if(string.IsNullOrWhiteSpace(text))
return "Please provide a text as input. You might copy the desired text from a document or a website.";
return null;
}
private string? ValidateCustomLanguage(string language)
{
if(this.selectedTargetLanguage == CommonLanguages.OTHER && string.IsNullOrWhiteSpace(language))
return "Please provide a custom language.";
return null;
}
private string SystemPromptLanguage()
{
var lang = this.selectedTargetLanguage switch
{
CommonLanguages.AS_IS => "the source language",
CommonLanguages.OTHER => this.customTargetLanguage,
_ => $"{this.selectedTargetLanguage.Name()}",
};
if (string.IsNullOrWhiteSpace(lang))
return "the source language";
return lang;
}
private async Task ProofreadText()
{
if (!this.inputIsValid)
return;
this.CreateChatThread();
var time = this.AddUserRequest(this.inputText);
this.correctedText = await this.AddAIResponseAsync(time);
await this.JsRuntime.GenerateAndShowDiff(this.inputText, this.correctedText);
}
private async Task CopyToClipboard()
{
await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, this.correctedText);
}
}

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools;
public readonly record struct ButtonData(string Text, string Icon, Color Color, string Tooltip, Func<Task> AsyncAction);

View File

@ -42,4 +42,12 @@ public static class CommonLanguageExtensions
return language.Name();
}
public static string NameSelectingOptional(this CommonLanguages language)
{
if(language is CommonLanguages.AS_IS)
return "Do not specify the language";
return language.Name();
}
}

View File

@ -0,0 +1,11 @@
using AIStudio.Components;
namespace AIStudio.Tools;
public static class JsRuntimeExtensions
{
public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2)
{
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantBase.ASSISTANT_RESULT_DIV_ID, AssistantBase.AFTER_RESULT_DIV_ID);
}
}

View File

@ -0,0 +1,19 @@
window.generateDiff = function (text1, text2, divDiff, divLegend) {
let wikEdDiff = new WikEdDiff();
let targetDiv = document.getElementById(divDiff)
targetDiv.innerHTML = wikEdDiff.diff(text1, text2);
targetDiv.classList.add('mud-typography-body1');
let legend = document.getElementById(divLegend);
legend.innerHTML = `
<div class="legend mt-2">
<h3>Legend</h3>
<ul class="mt-2">
<li><span class="wikEdDiffMarkRight" title="Moved block" id="wikEdDiffMark999" onmouseover="wikEdDiffBlockHandler(undefined, this, 'mouseover');"></span> Original block position</li>
<li><span title="+" class="wikEdDiffInsert">Inserted<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>text<span class="wikEdDiffNewline"> </span></span></li>
<li><span title="" class="wikEdDiffDelete">Deleted<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>text<span class="wikEdDiffNewline"> </span></span></li>
<li><span class="wikEdDiffBlockLeft" title="◀" id="wikEdDiffBlock999" onmouseover="wikEdDiffBlockHandler(undefined, this, 'mouseover');">Moved<span class="wikEdDiffSpace"><span class="wikEdDiffSpaceSymbol"></span> </span>block<span class="wikEdDiffNewline"> </span></span></li>
</ul>
</div>
`;
}

View File

@ -1,2 +1,4 @@
# v0.8.8, build 170
- Added a grammar and spell checker assistant
- Improved all assistants by showing a progress bar while processing
- Upgraded MudBlazor to v7.6.0

File diff suppressed because one or more lines are too long