diff --git a/app/MindWork AI Studio/Chat/ChatRoles.cs b/app/MindWork AI Studio/Chat/ChatRoles.cs new file mode 100644 index 00000000..6bbed0e9 --- /dev/null +++ b/app/MindWork AI Studio/Chat/ChatRoles.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Chat; + +public static class ChatRoles +{ + public static IEnumerable ChatTemplateRoles() + { + yield return ChatRole.SYSTEM; + yield return ChatRole.AI; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor new file mode 100644 index 00000000..fe5ee59f --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor @@ -0,0 +1,51 @@ +@inherits SettingsPanelBase + + + + @T("Your Chat Templates") + + + @T("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.") + + + + @T("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.") + + + + + + + + + # + @T("Chat Template Name") + @T("Actions") + + + @context.Num + @context.Name + + + + + + + + + + + + + + @if(this.SettingsManager.ConfigurationData.ChatTemplates.Count == 0) + { + + @T("No chat templates configured yet.") + + } + + + @T("Add Chat Template") + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs new file mode 100644 index 00000000..8db5a18d --- /dev/null +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelChatTemplates.razor.cs @@ -0,0 +1,74 @@ +using AIStudio.Dialogs; +using AIStudio.Settings; + +using DialogOptions = AIStudio.Dialogs.DialogOptions; + +namespace AIStudio.Components.Settings; + +public partial class SettingsPanelChatTemplates : SettingsPanelBase +{ + private async Task AddChatTemplate() + { + var dialogParameters = new DialogParameters + { + { x => x.IsEditing, false }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Add Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var addedChatTemplate = (ChatTemplate)dialogResult.Data!; + addedChatTemplate = addedChatTemplate with { Num = this.SettingsManager.ConfigurationData.NextChatTemplateNum++ }; + + this.SettingsManager.ConfigurationData.ChatTemplates.Add(addedChatTemplate); + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task EditChatTemplate(ChatTemplate chatTemplate) + { + // TODO: additionall messages übergeben + var dialogParameters = new DialogParameters + { + { x => x.DataNum, chatTemplate.Num }, + { x => x.DataId, chatTemplate.Id }, + { x => x.DataName, chatTemplate.Name }, + { x => x.DataSystemPrompt, chatTemplate.NeedToKnow }, + // { x => x.DataActions, chatTemplate.Actions }, + { x => x.IsEditing, true }, + // {x => x.AdditionalMessages, chatTemplate}, TODO + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Edit Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + var editedChatTemplate = (ChatTemplate)dialogResult.Data!; + this.SettingsManager.ConfigurationData.ChatTemplates[this.SettingsManager.ConfigurationData.ChatTemplates.IndexOf(chatTemplate)] = editedChatTemplate; + + await this.SettingsManager.StoreSettings(); + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } + + private async Task DeleteChatTemplate(ChatTemplate chatTemplate) + { + var dialogParameters = new DialogParameters + { + { "Message", string.Format(T("Are you sure you want to delete the chat template '{0}'?"), chatTemplate.Name) }, + }; + + var dialogReference = await this.DialogService.ShowAsync(T("Delete Chat Template"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + + this.SettingsManager.ConfigurationData.ChatTemplates.Remove(chatTemplate); + await this.SettingsManager.StoreSettings(); + + await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor new file mode 100644 index 00000000..b629c1b5 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor @@ -0,0 +1,130 @@ +@inherits MSGComponentBase +@inject ISnackbar Snackbar + + + + + @T("Store chat templates.") + + + + @T("Are you always using the same prompts and want a way of automatically using them?") + + + + @T("The name of the chat template is mandatory. Each chat template must have a unique name.") + + + @* ReSharper disable once CSharpWarnings::CS8974 *@ + + + + + + + + Additional messages + + Start using messages + + + Role + Entry + Actions + + + @context.Role + @context.Entry + + + + + + + + + @foreach (var role in availableRoles) + { + @role + } + + + + + + + + + + + + + + + + + + + + + @T("Cancel") + + + @if(this.IsEditing) + { + @T("Update") + } + else + { + @T("Add") + } + + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs new file mode 100644 index 00000000..d481f72b --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs @@ -0,0 +1,233 @@ +using AIStudio.Chat; +using AIStudio.Components; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class ChatTemplateDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + /// + /// The chat template's number in the list. + /// + [Parameter] + public uint DataNum { get; set; } + + /// + /// The chat template's ID. + /// + [Parameter] + public string DataId { get; set; } = Guid.NewGuid().ToString(); + + /// + /// The chat template name chosen by the user. + /// + [Parameter] + public string DataName { get; set; } = string.Empty; + + /// + /// What is the system prompt? + /// + [Parameter] + public string DataSystemPrompt { get; set; } = string.Empty; + + /// + /// Should the dialog be in editing mode? + /// + [Parameter] + public bool IsEditing { get; init; } + + [Parameter] + public List AdditionalMessages { get; set; } = []; + + [Inject] + private ILogger Logger { get; init; } = null!; + + private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new(); + + /// + /// The list of used chat template names. We need this to check for uniqueness. + /// + private List UsedNames { get; set; } = []; + + private bool dataIsValid; + private string[] dataIssues = []; + private string dataEditingPreviousName = string.Empty; + + private EntryItem messageEntryBeforeEdit; + private readonly List additionalMessagesEntries = []; + private readonly List availableRoles = ["User", "Assistant"]; + private bool initialAddButtonDisabled = false; + + // We get the form reference from Blazor code to validate it manually: + private MudForm form = null!; + + private ChatTemplate CreateChatTemplateSettings() => new() + { + Num = this.DataNum, + Id = this.DataId, + + Name = this.DataName, + NeedToKnow = this.DataSystemPrompt, + // AdditionalMessages = this.additionalMessagesEntries, + Actions = string.Empty, + }; + + private void RemoveMessage(EntryItem item) + { + this.additionalMessagesEntries.Remove(item); + this.Snackbar.Add("Entry removed", Severity.Info); + this.initialAddButtonDisabled = this.additionalMessagesEntries.Count > 0; + + // ChatRoles.ChatTemplateRoles() // TODO: -> darauf foreach für alle Rollen in der Tabelle + } + + private void AddInitialMessage() + { + var newEntry = new EntryItem + { + Role = availableRoles[0], // Default to first role ("User") + Entry = "Your message" + }; + + this.additionalMessagesEntries.Add(newEntry); + this.Snackbar.Add("Initial entry added", Severity.Success); + this.initialAddButtonDisabled = this.additionalMessagesEntries.Count > 0; + } + + private void AddNewMessageBelow(EntryItem currentItem) + { + + // Create new entry with a valid role + var newEntry = new EntryItem + { + Role = availableRoles.FirstOrDefault(role => role != currentItem.Role) ?? availableRoles[0], // Default to role not used in the previous entry + Entry = "Your message" + }; + + // Rest of the method remains the same + var index = this.additionalMessagesEntries.IndexOf(currentItem); + + if (index >= 0) + { + this.additionalMessagesEntries.Insert(index + 1, newEntry); + this.Snackbar.Add("New entry added", Severity.Success); + } + else + { + this.additionalMessagesEntries.Add(newEntry); + this.Snackbar.Add("New entry added", Severity.Success); + } + this.initialAddButtonDisabled = this.additionalMessagesEntries.Count > 0; + } + + private void BackupItem(object element) + { + this.messageEntryBeforeEdit = new() + { + Role = ((EntryItem)element).Role, + Entry = ((EntryItem)element).Entry + }; + } + + private void ItemHasBeenCommitted(object element) + { + this.Snackbar.Add("Changes saved", Severity.Success); + } + + private void ResetItemToOriginalValues(object element) + { + ((EntryItem)element).Role = this.messageEntryBeforeEdit.Role; + ((EntryItem)element).Entry = this.messageEntryBeforeEdit.Entry; + } + + public class EntryItem + { + public required string Role { get; set; } + public required string Entry { get; set; } + } + + #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.ChatTemplates.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 chat template. + // We just return this data to the parent component: + var addedChatTemplateSettings = this.CreateChatTemplateSettings(); + + if(this.IsEditing) + this.Logger.LogInformation($"Edited chat template '{addedChatTemplateSettings.Name}'."); + else + this.Logger.LogInformation($"Created chat template '{addedChatTemplateSettings.Name}'."); + + this.MudDialog.Close(DialogResult.Ok(addedChatTemplateSettings)); + } + + private string? ValidateSystemPrompt(string text) + { + if (string.IsNullOrWhiteSpace(this.DataSystemPrompt))// && string.IsNullOrWhiteSpace(this.DataActions)) + return T("Please enter the system prompt."); + + if(text.Length > 444) + return T("The text must not exceed 444 characters."); + + return null; + } + + private string? ValidateName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + return T("Please enter a name for the chat template."); + + if (name.Length > 40) + return T("The chat template 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 T("The chat template 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 9cc28223..f11f5a8d 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -16,6 +16,7 @@ } + diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs new file mode 100644 index 00000000..b53bf265 --- /dev/null +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -0,0 +1,64 @@ +using AIStudio.Tools.PluginSystem; + +namespace AIStudio.Settings; + +public readonly record struct ChatTemplate(uint Num, string Id, string Name, string NeedToKnow, string Actions) //, CategoryTypes.List AdditionalMessages) +{ + private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Profile).Namespace, nameof(Profile)); + + public static readonly Profile NO_PROFILE = new() + { + Name = TB("Use no profile"), + NeedToKnow = string.Empty, + Actions = string.Empty, + Id = Guid.Empty.ToString(), + Num = uint.MaxValue, + // AdditionalMessages = [], + }; + + #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 + + public string ToSystemPrompt() + { + if(this.Num == uint.MaxValue) + return string.Empty; + + var needToKnow = + $""" + What should you know about the user? + + ``` + {this.NeedToKnow} + ``` + """; + + var actions = + $""" + The user wants you to consider the following things. + + ``` + {this.Actions} + ``` + """; + + if (string.IsNullOrWhiteSpace(this.NeedToKnow)) + return actions; + + if (string.IsNullOrWhiteSpace(this.Actions)) + return needToKnow; + + return $""" + {needToKnow} + + {actions} + """; + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/Data.cs b/app/MindWork AI Studio/Settings/DataModel/Data.cs index 439427bc..695d2ad8 100644 --- a/app/MindWork AI Studio/Settings/DataModel/Data.cs +++ b/app/MindWork AI Studio/Settings/DataModel/Data.cs @@ -35,6 +35,11 @@ public sealed class Data /// List of configured profiles. /// public List Profiles { get; init; } = []; + + /// + /// List of configured chat templates. + /// + public List ChatTemplates { get; init; } = []; /// /// List of enabled plugins. @@ -60,6 +65,11 @@ public sealed class Data /// The next profile number to use. /// public uint NextProfileNum { get; set; } = 1; + + /// + /// The next chat template number to use. + /// + public uint NextChatTemplateNum { get; set; } = 1; public DataApp App { get; init; } = new();