diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor new file mode 100644 index 00000000..fb44439a --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor @@ -0,0 +1,93 @@ + + + + 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. + + + + 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. + + + + 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. + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + + + + + + Cancel + + @if(this.IsEditing) + { + @:Update + } + else + { + @:Add + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs new file mode 100644 index 00000000..7391c1d5 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs @@ -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!; + + /// + /// The profile's number in the list. + /// + [Parameter] + public uint DataNum { get; set; } + + /// + /// The profile's ID. + /// + [Parameter] + public string DataId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The profile name chosen by the user. + /// + [Parameter] + public string DataName { get; set; } = string.Empty; + + /// + /// What should the LLM know about you? + /// + [Parameter] + public string DataNeedToKnow { get; set; } = string.Empty; + + /// + /// What actions should the LLM take? + /// + [Parameter] + public string DataActions { get; set; } = string.Empty; + + /// + /// Should the dialog be in editing mode? + /// + [Parameter] + public bool IsEditing { get; init; } + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + [Inject] + private ILogger Logger { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + /// + /// The list of used profile names. We need this to check for uniqueness. + /// + private List 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(); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 620d3d1c..509813c8 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -74,6 +74,55 @@ + + Your Profiles + + 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. + + + + 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. + + + + + + + + + # + Profile Name + Actions + + + @context.Num + @context.Name + + + Edit + + + Delete + + + + + + @if(this.SettingsManager.ConfigurationData.Profiles.Count == 0) + { + No profiles configured yet. + } + + + Add Profile + + + diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index daa216b4..5c09f985 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor.cs +++ b/app/MindWork AI Studio/Pages/Settings.razor.cs @@ -160,6 +160,73 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable this.availableProviders.Add(new (provider.InstanceName, provider.Id)); } + #endregion + + #region Profile related + + private async Task AddProfile() + { + var dialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Add Profile", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var addedProfile = (Profile)dialogResult.Data!; + addedProfile = addedProfile with { Num = this.SettingsManager.ConfigurationData.NextProfileNum++ }; + + this.SettingsManager.ConfigurationData.Profiles.Add(addedProfile); + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task EditProfile(Profile profile) + { + var dialogParameters = new DialogParameters + { + { x => x.DataNum, profile.Num }, + { x => x.DataId, profile.Id }, + { x => x.DataName, profile.Name }, + { x => x.DataNeedToKnow, profile.NeedToKnow }, + { x => x.DataActions, profile.Actions }, + { x => x.IsEditing, true }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Edit Profile", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var editedProfile = (Profile)dialogResult.Data!; + this.SettingsManager.ConfigurationData.Profiles[this.SettingsManager.ConfigurationData.Profiles.IndexOf(profile)] = editedProfile; + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task DeleteProfile(Profile profile) + { + var dialogParameters = new DialogParameters + { + { "Message", $"Are you sure you want to delete the profile '{profile.Name}'?" }, + }; + + var dialogReference = await this.DialogService.ShowAsync("Delete Profile", dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + this.SettingsManager.ConfigurationData.Profiles.Remove(profile); + await this.SettingsManager.StoreSettings(); + + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + #endregion #region Implementation of IMessageBusReceiver diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 586c763d..f1dc8867 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -15,12 +15,22 @@ public sealed class Data /// List of configured providers. /// public List Providers { get; init; } = []; + + /// + /// List of configured profiles. + /// + public List Profiles { get; init; } = []; /// /// The next provider number to use. /// public uint NextProviderNum { get; set; } = 1; + /// + /// The next profile number to use. + /// + public uint NextProfileNum { get; set; } = 1; + public DataApp App { get; init; } = new(); public DataChat Chat { get; init; } = new(); diff --git a/app/MindWork AI Studio/Settings/Profile.cs b/app/MindWork AI Studio/Settings/Profile.cs new file mode 100644 index 00000000..cbcdc5c3 --- /dev/null +++ b/app/MindWork AI Studio/Settings/Profile.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Settings; + +public readonly record struct Profile(uint Num, string Id, string Name, string NeedToKnow, string Actions) +{ + #region Overrides of ValueType + + /// + /// Returns a string that represents the profile in a human-readable format. + /// + /// A string that represents the profile in a human-readable format. + public override string ToString() => this.Name; + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v0.9.7.md b/app/MindWork AI Studio/wwwroot/changelog/v0.9.7.md index 81c3b28d..4cc05a15 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v0.9.7.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.7.md @@ -1,4 +1,5 @@ # v0.9.7, build 182 (2024-09-09 xx:xx UTC) +- Added the possibility to define multiple profiles in the settings. Use profiles to share some information about you with the AI. - Added an introductory description to the provider settings. - Added an indicator for the current and maximal length of the provider instance name. - Fixed the bug that the model name for Fireworks was not loaded when editing the provider settings.