Add profiles (#132)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2024-09-08 21:01:51 +02:00
1 parent 00f45f8998
commit d7c124926b
32 files changed
+659 -36

No files matched your search

@@ -0,0 +1,93 @@
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Store personal data about yourself in various profiles so that the AIs know your personal context.
This saves you from having to explain your context each time, for example, in every chat. When you
have different roles, you can create a profile for each role.
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
Are you a project manager in a research facility? You might want to create a profile for your project
management activities, one for your scientific work, and a profile for when you need to write program
code. In these profiles, you can record how much experience you have or which methods you like or
dislike using. Later, you can choose when and where you want to use each profile.
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
The name of the profile is mandatory. Each profile must have a unique name. Whether you provide
information about yourself or only fill out the actions is up to you. Only one of these pieces
is required.
</MudJustifiedText>
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
T="string"
@bind-Text="@this.DataName"
Label="Profile Name"
Class="mb-3"
Immediate="@true"
MaxLength="40"
Counter="40"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Badge"
AdornmentColor="Color.Info"
Validation="@this.ValidateName"
Variant="Variant.Outlined"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
/>
<MudTextField
T="string"
@bind-Text="@this.DataNeedToKnow"
Validation="@this.ValidateNeedToKnow"
AdornmentIcon="@Icons.Material.Filled.ListAlt"
Adornment="Adornment.Start"
Immediate="@true"
Label="What should the AI know about you?"
Variant="Variant.Outlined"
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="444"
Counter="444"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?"
/>
<MudTextField
T="string"
@bind-Text="@this.DataActions"
Validation="@this.ValidateActions"
AdornmentIcon="@Icons.Material.Filled.ListAlt"
Adornment="Adornment.Start"
Immediate="@true"
Label="What should the AI do for you?"
Variant="Variant.Outlined"
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="256"
Counter="256"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
/>
</MudForm>
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">Cancel</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if(this.IsEditing)
{
@:Update
}
else
{
@:Add
}
</MudButton>
</DialogActions>
</MudDialog>
@@ -0,0 +1,168 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Dialogs;
public partial class ProfileDialog : ComponentBase
{
[CascadingParameter]
private MudDialogInstance MudDialog { get; set; } = null!;
/// <summary>
/// The profile's number in the list.
/// </summary>
[Parameter]
public uint DataNum { get; set; }
/// <summary>
/// The profile's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The profile name chosen by the user.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// What should the LLM know about you?
/// </summary>
[Parameter]
public string DataNeedToKnow { get; set; } = string.Empty;
/// <summary>
/// What actions should the LLM take?
/// </summary>
[Parameter]
public string DataActions { get; set; } = string.Empty;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Inject]
private SettingsManager SettingsManager { get; init; } = null!;
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used profile names. We need this to check for uniqueness.
/// </summary>
private List<string> UsedNames { get; set; } = [];
private bool dataIsValid;
private string[] dataIssues = [];
private string dataEditingPreviousName = string.Empty;
// We get the form reference from Blazor code to validate it manually:
private MudForm form = null!;
private Profile CreateProfileSettings() => new()
{
Num = this.DataNum,
Id = this.DataId,
Name = this.DataName,
NeedToKnow = this.DataNeedToKnow,
Actions = this.DataActions,
};
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
{
// Configure the spellchecking for the instance name input:
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
// Load the used instance names:
this.UsedNames = this.SettingsManager.ConfigurationData.Profiles.Select(x => x.Name.ToLowerInvariant()).ToList();
// When editing, we need to load the data:
if(this.IsEditing)
{
this.dataEditingPreviousName = this.DataName.ToLowerInvariant();
}
await base.OnInitializedAsync();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Reset the validation when not editing and on the first render.
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
await base.OnAfterRenderAsync(firstRender);
}
#endregion
private async Task Store()
{
await this.form.Validate();
// When the data is not valid, we don't store it:
if (!this.dataIsValid)
return;
// Use the data model to store the profile.
// We just return this data to the parent component:
var addedProfileSettings = this.CreateProfileSettings();
if(this.IsEditing)
this.Logger.LogInformation($"Edited profile '{addedProfileSettings.Name}'.");
else
this.Logger.LogInformation($"Created profile '{addedProfileSettings.Name}'.");
this.MudDialog.Close(DialogResult.Ok(addedProfileSettings));
}
private string? ValidateNeedToKnow(string text)
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return "Please enter what the LLM should know about you and/or what actions it should take.";
if(text.Length > 444)
return "The text must not exceed 444 characters.";
return null;
}
private string? ValidateActions(string text)
{
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return "Please enter what the LLM should know about you and/or what actions it should take.";
if(text.Length > 256)
return "The text must not exceed 256 characters.";
return null;
}
private string? ValidateName(string name)
{
if (string.IsNullOrWhiteSpace(name))
return "Please enter a profile name.";
if (name.Length > 40)
return "The profile name must not exceed 40 characters.";
// The instance name must be unique:
var lowerName = name.ToLowerInvariant();
if (lowerName != this.dataEditingPreviousName && this.UsedNames.Contains(lowerName))
return "The profile name must be unique; the chosen name is already in use.";
return null;
}
private void Cancel() => this.MudDialog.Cancel();
}
@@ -83,6 +83,9 @@
@bind-Text="@this.DataInstanceName"
Label="Instance Name"
Class="mb-3"
MaxLength="40"
Counter="40"
Immediate="@true"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lightbulb"
AdornmentColor="Color.Info"
@@ -1,5 +1,3 @@
using System.Text.RegularExpressions;
using AIStudio.Provider;
using AIStudio.Settings;
@@ -128,6 +126,10 @@ public partial class ProviderDialog : ComponentBase
{
this.dataEditingPreviousInstanceName = this.DataInstanceName.ToLowerInvariant();
// When using Fireworks, we must copy the model name:
if (this.DataProvider is Providers.FIREWORKS)
this.dataManuallyModel = this.DataModel.Id;
//
// We cannot load the API key for self-hosted providers:
//
@@ -245,40 +247,14 @@ public partial class ProviderDialog : ComponentBase
return null;
}
[GeneratedRegex(@"^[a-zA-Z0-9\-_. ]+$")]
private static partial Regex InstanceNameRegex();
private static readonly string[] RESERVED_NAMES = ["CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"];
private string? ValidatingInstanceName(string instanceName)
{
if (string.IsNullOrWhiteSpace(instanceName))
return "Please enter an instance name.";
if (instanceName.StartsWith(' ') || instanceName.StartsWith('.'))
return "The instance name must not start with a space or a dot.";
if (instanceName.EndsWith(' ') || instanceName.EndsWith('.'))
return "The instance name must not end with a space or a dot.";
if (instanceName.StartsWith('-') || instanceName.StartsWith('_'))
return "The instance name must not start with a hyphen or an underscore.";
if (instanceName.Length > 255)
return "The instance name must not exceed 255 characters.";
if (!InstanceNameRegex().IsMatch(instanceName))
return "The instance name must only contain letters, numbers, spaces, hyphens, underscores, and dots.";
if (instanceName.Contains(" "))
return "The instance name must not contain consecutive spaces.";
if (RESERVED_NAMES.Contains(instanceName.ToUpperInvariant()))
return "This name is reserved and cannot be used.";
if (instanceName.Any(c => Path.GetInvalidFileNameChars().Contains(c)))
return "The instance name contains invalid characters.";
if (instanceName.Length > 40)
return "The instance name must not exceed 40 characters.";
// The instance name must be unique:
var lowerInstanceName = instanceName.ToLowerInvariant();