diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor b/app/MindWork AI Studio/Assistants/AssistantBase.razor index 9d7673e..ba18f75 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor @@ -93,6 +93,11 @@ Reset + + @if (this.AllowProfiles) + { + + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs index 9608b1c..c63659f 100644 --- a/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs +++ b/app/MindWork AI Studio/Assistants/AssistantBase.razor.cs @@ -55,6 +55,8 @@ public abstract partial class AssistantBase : ComponentBase private protected virtual RenderFragment? Body => null; protected virtual bool ShowResult => true; + + protected virtual bool AllowProfiles => true; protected virtual bool ShowDedicatedProgress => false; @@ -72,6 +74,7 @@ public abstract partial class AssistantBase : ComponentBase private ContentBlock? resultingContentBlock; private string[] inputIssues = []; private bool isProcessing; + private Profile currentProfile = Profile.NO_PROFILE; #region Overrides of ComponentBase @@ -79,6 +82,7 @@ public abstract partial class AssistantBase : ComponentBase { this.MightPreselectValues(); this.providerSettings = this.SettingsManager.GetPreselectedProvider(this.Component); + this.currentProfile = this.SettingsManager.GetPreselectedProfile(this.Component); await base.OnInitializedAsync(); } @@ -118,7 +122,12 @@ public abstract partial class AssistantBase : ComponentBase ChatId = Guid.NewGuid(), Name = string.Empty, Seed = this.RNG.Next(), - SystemPrompt = this.SystemPrompt, + SystemPrompt = !this.AllowProfiles ? this.SystemPrompt : + $""" + {this.SystemPrompt} + + {this.currentProfile.ToSystemPrompt()} + """, Blocks = [], }; } diff --git a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs index f0dd044..21227df 100644 --- a/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs +++ b/app/MindWork AI Studio/Assistants/GrammarSpelling/AssistantGrammarSpelling.razor.cs @@ -22,6 +22,8 @@ public partial class AssistantGrammarSpelling : AssistantBaseCore 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 AllowProfiles => false; protected override bool ShowResult => false; diff --git a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs index f23f710..e239c8f 100644 --- a/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs +++ b/app/MindWork AI Studio/Assistants/IconFinder/AssistantIconFinder.razor.cs @@ -24,6 +24,8 @@ public partial class AssistantIconFinder : AssistantBaseCore related to the keyword "buildings" might be the best match. Provide your keywords in a Markdown list without quotation marks. """; + + protected override bool AllowProfiles => false; protected override IReadOnlyList FooterButtons => []; diff --git a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs index a7ada74..6d8aef6 100644 --- a/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs +++ b/app/MindWork AI Studio/Assistants/RewriteImprove/AssistantRewriteImprove.razor.cs @@ -24,6 +24,8 @@ public partial class AssistantRewriteImprove : AssistantBaseCore You follow the rules according to {this.SystemPromptLanguage()} in all your changes. """; + protected override bool AllowProfiles => false; + protected override bool ShowResult => false; protected override bool ShowDedicatedProgress => true; diff --git a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs index 032a50a..c238aac 100644 --- a/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs +++ b/app/MindWork AI Studio/Assistants/Synonym/AssistantSynonyms.razor.cs @@ -47,6 +47,8 @@ public partial class AssistantSynonyms : AssistantBaseCore the {this.SystemPromptLanguage()} language. """; + protected override bool AllowProfiles => false; + protected override IReadOnlyList FooterButtons => []; protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with diff --git a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs index c8461e2..20c0300 100644 --- a/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs +++ b/app/MindWork AI Studio/Assistants/TextSummarizer/AssistantTextSummarizer.razor.cs @@ -25,6 +25,8 @@ public partial class AssistantTextSummarizer : AssistantBaseCore a summary with the requested complexity. In any case, do not add any information. """; + protected override bool AllowProfiles => false; + protected override IReadOnlyList FooterButtons => []; protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with diff --git a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs index 8485b16..29cc0ef 100644 --- a/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs +++ b/app/MindWork AI Studio/Assistants/Translation/AssistantTranslation.razor.cs @@ -21,6 +21,8 @@ public partial class AssistantTranslation : AssistantBaseCore language requires, e.g., shorter sentences, you should split the text into shorter sentences. """; + protected override bool AllowProfiles => false; + protected override IReadOnlyList FooterButtons => []; protected override ChatThread ConvertToChatThread => (this.chatThread ?? new()) with diff --git a/app/MindWork AI Studio/Components/MudJustifiedText.cs b/app/MindWork AI Studio/Components/MudJustifiedText.cs new file mode 100644 index 0000000..887308e --- /dev/null +++ b/app/MindWork AI Studio/Components/MudJustifiedText.cs @@ -0,0 +1,16 @@ +namespace AIStudio.Components; + +public class MudJustifiedText : MudText +{ + #region Overrides of ComponentBase + + protected override async Task OnInitializedAsync() + { + this.Align = Align.Justify; + this.Style = "hyphens: auto; word-break: auto-phrase;"; + + await base.OnInitializedAsync(); + } + + #endregion +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/MudTextList.razor b/app/MindWork AI Studio/Components/MudTextList.razor index bb70247..f0ec629 100644 --- a/app/MindWork AI Studio/Components/MudTextList.razor +++ b/app/MindWork AI Studio/Components/MudTextList.razor @@ -2,7 +2,7 @@ @foreach(var item in this.Items) { - @item.Header: @item.Text + @item.Header: @item.Text } \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor b/app/MindWork AI Studio/Components/ProfileSelection.razor new file mode 100644 index 0000000..a832ec6 --- /dev/null +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor @@ -0,0 +1,10 @@ + + + @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles()) + { + + @profile.Name + + } + + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProfileSelection.razor.cs b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs new file mode 100644 index 0000000..55f2fa9 --- /dev/null +++ b/app/MindWork AI Studio/Components/ProfileSelection.razor.cs @@ -0,0 +1,28 @@ +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +public partial class ProfileSelection : ComponentBase +{ + [Parameter] + public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE; + + [Parameter] + public EventCallback CurrentProfileChanged { get; set; } + + [Parameter] + public string MarginLeft { get; set; } = "ml-3"; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + private string MarginClass => $"{this.MarginLeft}"; + + private async Task SelectionChanged(Profile profile) + { + this.CurrentProfile = profile; + await this.CurrentProfileChanged.InvokeAsync(profile); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor new file mode 100644 index 0000000..fb44439 --- /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 0000000..7391c1d --- /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/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index c6fd8dc..82cb514 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -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" diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index 02d29d2..0d55ade 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -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(); diff --git a/app/MindWork AI Studio/Pages/Chat.razor b/app/MindWork AI Studio/Pages/Chat.razor index f87bdde..3d5d3b7 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor +++ b/app/MindWork AI Studio/Pages/Chat.razor @@ -69,6 +69,8 @@ } + + diff --git a/app/MindWork AI Studio/Pages/Chat.razor.cs b/app/MindWork AI Studio/Pages/Chat.razor.cs index e3ff30e..e94a9ca 100644 --- a/app/MindWork AI Studio/Pages/Chat.razor.cs +++ b/app/MindWork AI Studio/Pages/Chat.razor.cs @@ -35,6 +35,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable private static readonly Dictionary USER_INPUT_ATTRIBUTES = new(); private AIStudio.Settings.Provider providerSettings; + private Profile currentProfile = Profile.NO_PROFILE; private ChatThread? chatThread; private bool hasUnsavedChanges; private bool isStreaming; @@ -61,6 +62,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES); this.providerSettings = this.SettingsManager.GetPreselectedProvider(Tools.Components.CHAT); + this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT); var deferredContent = MessageBus.INSTANCE.CheckDeferredMessages(Event.SEND_TO_CHAT).FirstOrDefault(); if (deferredContent is not null) { @@ -118,6 +120,22 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable private bool CanThreadBeSaved => this.chatThread is not null && this.chatThread.Blocks.Count > 0; private string TooltipAddChatToWorkspace => $"Start new chat in workspace \"{this.currentWorkspaceName}\""; + + private void ProfileWasChanged(Profile profile) + { + this.currentProfile = profile; + if(this.chatThread is null) + return; + + this.chatThread = this.chatThread with + { + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """ + }; + } private async Task SendMessage() { @@ -135,7 +153,11 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable ChatId = Guid.NewGuid(), Name = threadName, Seed = this.RNG.Next(), - SystemPrompt = SystemPrompts.DEFAULT, + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """, Blocks = [], }; } @@ -320,7 +342,11 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable ChatId = Guid.NewGuid(), Name = string.Empty, Seed = this.RNG.Next(), - SystemPrompt = "You are a helpful assistant!", + SystemPrompt = $""" + {SystemPrompts.DEFAULT} + + {this.currentProfile.ToSystemPrompt()} + """, Blocks = [], }; } diff --git a/app/MindWork AI Studio/Pages/Settings.razor b/app/MindWork AI Studio/Pages/Settings.razor index 02586ce..32f979c 100644 --- a/app/MindWork AI Studio/Pages/Settings.razor +++ b/app/MindWork AI Studio/Pages/Settings.razor @@ -11,6 +11,12 @@ Configured Providers + + What we call a provider is the combination of an LLM provider such as OpenAI and a model like GPT-4o. + You can configure as many providers as you want. This way, you can use the appropriate model for each + task. As an LLM provider, you can also choose local providers. However, to use this app, you must + configure at least one provider. + @@ -68,12 +74,62 @@ + + 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 + + + + @@ -82,6 +138,7 @@ + @@ -125,6 +182,7 @@ } + @@ -167,13 +225,13 @@ - @if (this.SettingsManager.ConfigurationData.Agenda.PreselectedTargetLanguage is CommonLanguages.OTHER) { } + @@ -215,6 +273,7 @@ } + @@ -225,6 +284,7 @@ + diff --git a/app/MindWork AI Studio/Pages/Settings.razor.cs b/app/MindWork AI Studio/Pages/Settings.razor.cs index daa216b..5c09f98 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/ConfigurationSelectData.cs b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs index 2266b6a..c8dfd39 100644 --- a/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs +++ b/app/MindWork AI Studio/Settings/ConfigurationSelectData.cs @@ -130,4 +130,10 @@ public static class ConfigurationSelectDataFactory foreach (var voice in Enum.GetValues()) yield return new(voice.Name(), voice); } + + public static IEnumerable> GetProfilesData(IEnumerable profiles) + { + foreach (var profile in profiles.GetAllProfiles()) + yield return new(profile.Name, profile.Id); + } } \ 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 586c763..f1dc886 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/DataModel/DataAgenda.cs b/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs index 46ef668..3960274 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataAgenda.cs @@ -55,4 +55,9 @@ public sealed class DataAgenda /// Preselect a agenda provider? /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs index 55b9e93..ce1f475 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs @@ -27,4 +27,9 @@ public sealed class DataApp /// Should we preselect a provider for the entire app? /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Should we preselect a profile for the entire app? + /// + public string PreselectedProfile { get; set; } = string.Empty; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs index eca8569..f68865f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataChat.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataChat.cs @@ -16,6 +16,11 @@ public sealed class DataChat /// Should we preselect a provider for the chat? /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; /// /// Should we show the latest message after loading? When false, we show the first (aka oldest) message. diff --git a/app/MindWork AI Studio/Settings/DataModel/DataCoding.cs b/app/MindWork AI Studio/Settings/DataModel/DataCoding.cs index 7608f61..f83c616 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataCoding.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataCoding.cs @@ -28,4 +28,9 @@ public sealed class DataCoding /// Which coding provider should be preselected? /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs b/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs index ab659fc..a913c7f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataEMail.cs @@ -28,6 +28,11 @@ public sealed class DataEMail /// Preselect a provider? /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; /// /// Preselect a greeting phrase? diff --git a/app/MindWork AI Studio/Settings/DataModel/DataLegalCheck.cs b/app/MindWork AI Studio/Settings/DataModel/DataLegalCheck.cs index 80dc53f..c72b157 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataLegalCheck.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataLegalCheck.cs @@ -26,4 +26,9 @@ public class DataLegalCheck /// The preselected translator provider. /// public string PreselectedProvider { get; set; } = string.Empty; + + /// + /// Preselect a profile? + /// + public string PreselectedProfile { get; set; } = string.Empty; } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Profile.cs b/app/MindWork AI Studio/Settings/Profile.cs new file mode 100644 index 0000000..b7a7da6 --- /dev/null +++ b/app/MindWork AI Studio/Settings/Profile.cs @@ -0,0 +1,59 @@ +namespace AIStudio.Settings; + +public readonly record struct Profile(uint Num, string Id, string Name, string NeedToKnow, string Actions) +{ + public static readonly Profile NO_PROFILE = new() + { + Name = "Use no profile", + NeedToKnow = string.Empty, + Actions = string.Empty, + Id = Guid.Empty.ToString(), + Num = uint.MaxValue, + }; + + #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/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 36441bc..428c518 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -138,4 +138,24 @@ public sealed class SettingsManager(ILogger logger) return this.ConfigurationData.Providers.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProvider); } + + public Profile GetPreselectedProfile(Tools.Components component) + { + var preselection = component switch + { + Tools.Components.CHAT => this.ConfigurationData.Chat.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Chat.PreselectedProfile) : default, + Tools.Components.AGENDA_ASSISTANT => this.ConfigurationData.Agenda.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Agenda.PreselectedProfile) : default, + Tools.Components.CODING_ASSISTANT => this.ConfigurationData.Coding.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.Coding.PreselectedProfile) : default, + Tools.Components.EMAIL_ASSISTANT => this.ConfigurationData.EMail.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.EMail.PreselectedProfile) : default, + Tools.Components.LEGAL_CHECK_ASSISTANT => this.ConfigurationData.LegalCheck.PreselectOptions ? this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.LegalCheck.PreselectedProfile) : default, + + _ => default, + }; + + if (preselection != default) + return preselection; + + preselection = this.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == this.ConfigurationData.App.PreselectedProfile); + return preselection != default ? preselection : Profile.NO_PROFILE; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/ProfileExtensions.cs b/app/MindWork AI Studio/Tools/ProfileExtensions.cs new file mode 100644 index 0000000..53cb256 --- /dev/null +++ b/app/MindWork AI Studio/Tools/ProfileExtensions.cs @@ -0,0 +1,13 @@ +using AIStudio.Settings; + +namespace AIStudio.Tools; + +public static class ProfileExtensions +{ + public static IEnumerable GetAllProfiles(this IEnumerable profiles) + { + yield return Profile.NO_PROFILE; + foreach (var profile in profiles) + yield return profile; + } +} \ 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 new file mode 100644 index 0000000..1427636 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v0.9.7.md @@ -0,0 +1,10 @@ +# 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 profiles to the chat interface. You can now select a profile for each chat or even change the profile during a chat. +- Added profiles to some assistants. It makes no sense to have profiles for, e.g., translation, etc. +- Added the possibility to preselect any of your profiles as the default profile for the entire app or configure individual profiles for assistants and chats. +- 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. +- Improved hyphenation for continuous text so that the rules of the respective language are taken into account where possible. +- Improved the rules for provider names: unnecessary restrictions from earlier versions have been removed. You can now use emojis in your provider names when you like. \ No newline at end of file