Skip to content

Commit

Permalink
feat: Add Autocomplete support
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Jun 2, 2024
1 parent 3c0b4e4 commit 3641424
Show file tree
Hide file tree
Showing 14 changed files with 398 additions and 98 deletions.
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js]
- [Comments](./docs/Comments/Readme.md)
- [Storage Provider](./docs/Storage/Readme.md)
- [Search Engine Optimization (SEO)](./docs/SEO/Readme.md)
- [AI Autocomplete](./docs/Autocomplete/Readme.md)

## Installation

Expand Down
5 changes: 5 additions & 0 deletions docs/Autocomplete/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### Autocomplete
The blog can utilize Microsofts **Semantic Kernel** to either generate or enhance the current blog post.
There are toggles to provide the AI with the Title, Descritpion, Markdown Content and Tags. You can also instruct the AI only to append content and not rewrite the whole article.

You can find the configuration in the `appsettings.json` file under the `AI` section.
11 changes: 11 additions & 0 deletions docs/Setup/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ The appsettings.json file has a lot of options to customize the content of the b
"BackgroundUrl": "assets/profile-background.webp",
"ProfilePictureUrl": "assets/profile-picture.webp"
},
"AI": {
"DeploymentName": "gpt-4o",
"EndpointUrl": "https://the-url",
"ModelId": "gpt-4o",
"ApiKey": "key"
},
"PersistenceProvider": "InMemory",
"ConnectionString": "",
"DatabaseName": "",
Expand Down Expand Up @@ -63,6 +69,11 @@ The appsettings.json file has a lot of options to customize the content of the b
| Introduction | | Is used for the introduction part of the blog |
| Description | MarkdownString | Small introduction text for yourself. This is also used for `<meta name="description">` tag. For this the markup will be converted to plain text |
| BackgroundUrl | string | Url or path to the background image. (Optional) |
| AI | | The AI section used for Semantic Kernel |
| DeploymentName | string | Name of the deployment |
| EndpointUrl | string | Url to the endpoint |
| ModelId | string | Model Id |
| ApiKey | string | Api Key |
| ProfilePictureUrl | string | Url or path to your profile picture |
| PersistenceProvider | string | Declares the type of the storage provider (one of the following: `SqlServer`, `Sqlite`, `RavenDb`, `InMemory`, `MySql`). More in-depth explanation [here](./../Storage/Readme.md) |
| ConnectionString | string | Is used for connection to a database. Not used when `InMemoryStorageProvider` is used |
Expand Down
19 changes: 19 additions & 0 deletions src/LinkDotNet.Blog.Domain/AiSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
namespace LinkDotNet.Blog.Domain;

public class AiSettings
{
public const string AiSettingsSection = "AI";

public string DeploymentName { get; set; }

public string EndpointUrl { get; set; }

public string ModelId { get; set; }

public string ApiKey { get; set; }

public bool IsEnabled => !string.IsNullOrEmpty(DeploymentName)
&& !string.IsNullOrEmpty(EndpointUrl)
&& !string.IsNullOrEmpty(ModelId)
&& !string.IsNullOrEmpty(ApiKey);
}
13 changes: 13 additions & 0 deletions src/LinkDotNet.Blog.Web/ConfigurationExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public static void AddConfiguration(this IServiceCollection services)
.AddApplicationConfiguration()
.AddAuthenticationConfigurations()
.AddIntroductionConfigurations()
.AddAiConfigurations()
.AddSocialConfigurations()
.AddProfileInformationConfigurations()
.AddGiscusConfiguration()
Expand Down Expand Up @@ -66,6 +67,18 @@ private static IServiceCollection AddIntroductionConfigurations(this IServiceCol
return services;
}

private static IServiceCollection AddAiConfigurations(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);

services.AddOptions<AiSettings>()
.Configure<IConfiguration>((settings, config) =>
{
config.GetSection(AiSettings.AiSettingsSection).Bind(settings);
});
return services;
}

private static IServiceCollection AddSocialConfigurations(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
@using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Services
<ModalDialog @ref="Dialog" Title="AutoComplete Content">
<form>
<div class="mb-3">
<label for="systemMessage" class="form-label">Message</label>
<TextAreaWithShortcuts Class="form-control" Id="systemMessage" Rows="2" Placeholder="Enter message..." @bind-Value="@options.Prompt"></TextAreaWithShortcuts>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's title in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeTitle" @bind-value="@options.IncludeTitle">
<label class="form-check-label" for="includeTitle">Include Title</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's short description in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeShortDescription" @bind-value="@options.IncludeShortDescription">
<label class="form-check-label" for="includeShortDescription">Include Short Description</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's tags in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeTags" @bind-value="@options.IncludeTags">
<label class="form-check-label" for="includeTags">Include Tags</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Include the blog post's content in the AI input.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="includeContent" @bind-value="@options.IncludeContent">
<label class="form-check-label" for="includeContent">Include Content</label>
</div>
</div>

<div class="mb-3">
<small class="text-muted">Keep original text and append AI-generated content, or allow AI to rewrite the content.</small>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="keepOriginalText" @bind-value="@options.KeepOriginalText">
<label class="form-check-label" for="keepOriginalText">Keep Original Text</label>
</div>
</div>

<button type="button" class="btn btn-primary btn-sm" @onclick="Generate">Generate</button>

<div class="mt-3">
<label for="outputField" class="form-label">Output</label>
<textarea class="form-control" id="outputField" rows="6" readonly>@options.Content</textarea>
</div>

<div class="d-flex mt-3 justify-content-end gap-3">
<button type="button" class="btn btn-secondary" @onclick="Cancel">Cancel</button>
<button type="button" class="btn btn-success" @onclick="Save" disabled="@(!options.AllowSave)">Save</button>
</div>
</form>
</ModalDialog>

@code {
[Inject] private AutocompleteService AutocompleteService { get; set; }

[Parameter] public CreateNewModel Model { get; set; }
[Parameter] public EventCallback<string> ContentGenerated { get; set; }

private ModalDialog Dialog { get; set; }
private Options options = new ();

public void Open()
{
Dialog.Open();
StateHasChanged();
}

private void Cancel()
{
Dialog.Close();
}

private async Task Generate()
{
var completeOptions = new AutocompleteOptions(
options.IncludeTitle ? Model.Title : string.Empty,
options.IncludeShortDescription ? Model.ShortDescription : string.Empty,
options.IncludeTags ? Model.Tags : string.Empty,
options.IncludeContent ? Model.Content : string.Empty,
options.Prompt,
options.KeepOriginalText);
options.Content = await AutocompleteService.GetAutocomplete(completeOptions);
options.AllowSave = true;
}

private async Task Save()
{
await ContentGenerated.InvokeAsync(options.Content);
Dialog.Close();
options = new();
}

private sealed class Options
{
public string Prompt { get; set; }
public bool IncludeTitle { get; set; }
public bool IncludeShortDescription { get; set; }
public bool IncludeTags { get; set; }
public bool IncludeContent { get; set; }
public bool KeepOriginalText { get; set; }
public string Content { get; set; }

public bool AllowSave { get; set; }
}
}
Loading

0 comments on commit 3641424

Please sign in to comment.