First version before review from Thorsten

This commit is contained in:
Peer Schütt 2025-05-16 13:14:04 +02:00
parent 85a867459e
commit 5f2da6acf6
8 changed files with 573 additions and 0 deletions

View File

@ -0,0 +1,10 @@
namespace AIStudio.Chat;
public static class ChatRoles
{
public static IEnumerable<ChatRole> ChatTemplateRoles()
{
yield return ChatRole.SYSTEM;
yield return ChatRole.AI;
}
}

View File

@ -0,0 +1,51 @@
@inherits SettingsPanelBase
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Person4" HeaderText="@T("Configure Chat Templates")">
<MudText Typo="Typo.h4" Class="mb-3">
@T("Your Chat Templates")
</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@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.")
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@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.")
</MudJustifiedText>
<MudTable Items="@this.SettingsManager.ConfigurationData.ChatTemplates" Hover="@true" Class="border-dashed border rounded-lg">
<ColGroup>
<col style="width: 3em;"/>
<col/>
<col style="width: 40em;"/>
</ColGroup>
<HeaderContent>
<MudTh>#</MudTh>
<MudTh>@T("Chat Template Name")</MudTh>
<MudTh>@T("Actions")</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd>@context.Num</MudTd>
<MudTd>@context.Name</MudTd>
<MudTd>
<MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap">
<MudTooltip Text="@T("Edit")">
<MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditChatTemplate(context)"/>
</MudTooltip>
<MudTooltip Text="@T("Delete")">
<MudIconButton Variant="Variant.Filled" Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteChatTemplate(context)"/>
</MudTooltip>
</MudStack>
</MudTd>
</RowTemplate>
</MudTable>
@if(this.SettingsManager.ConfigurationData.ChatTemplates.Count == 0)
{
<MudText Typo="Typo.h6" Class="mt-3">
@T("No chat templates configured yet.")
</MudText>
}
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddChatTemplate">
@T("Add Chat Template")
</MudButton>
</ExpansionPanel>

View File

@ -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<ChatTemplateDialog>
{
{ x => x.IsEditing, false },
};
var dialogReference = await this.DialogService.ShowAsync<ChatTemplateDialog>(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<bool>(this, Event.CONFIGURATION_CHANGED);
}
private async Task EditChatTemplate(ChatTemplate chatTemplate)
{
// TODO: additionall messages übergeben
var dialogParameters = new DialogParameters<ChatTemplateDialog>
{
{ 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<ChatTemplateDialog>(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<bool>(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<ConfirmDialog>(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<bool>(this, Event.CONFIGURATION_CHANGED);
}
}

View File

@ -0,0 +1,130 @@
@inherits MSGComponentBase
@inject ISnackbar Snackbar
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Store chat templates.")
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("Are you always using the same prompts and want a way of automatically using them?")
</MudJustifiedText>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@T("The name of the chat template is mandatory. Each chat template must have a unique name.")
</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="@T("Chat Template 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.DataSystemPrompt"
Validation="@this.ValidateSystemPrompt"
AdornmentIcon="@Icons.Material.Filled.ListAlt"
Adornment="Adornment.Start"
Immediate="@true"
Label="@T("What system prompt do you want to use?")"
Variant="Variant.Outlined"
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="444"
Counter="444"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI your system prompt.")"
/>
<MudTable Items="@additionalMessagesEntries" RowEditPreview="BackupItem" RowEditCancel="ResetItemToOriginalValues" RowEditCommit="ItemHasBeenCommitted" CanCancelEdit="true" CommitEditTooltip="Commit Changes" Elevation="10" Outlined="true">
<ToolBarContent>
<MudText Typo="Typo.h6">Additional messages</MudText>
<MudSpacer />
<MudButton Color="Color.Primary" Variant="Variant.Filled" OnClick="AddInitialMessage" StartIcon="@Icons.Material.Filled.Add" Disabled="@initialAddButtonDisabled">Start using messages</MudButton>
</ToolBarContent>
<HeaderContent>
<MudTh>Role</MudTh>
<MudTh>Entry</MudTh>
<MudTh>Actions</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Role">@context.Role</MudTd>
<MudTd DataLabel="Message">@context.Entry</MudTd>
<MudTd style="text-align: center">
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Success"
Size="Size.Small"
OnClick="@(() => AddNewMessageBelow(context))"
/>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveMessage(context))"
/>
</MudTd>
</RowTemplate>
<RowEditingTemplate>
<MudTd DataLabel="Role">
<MudSelect @bind-Value="context.Role" Required>
@foreach (var role in availableRoles)
{
<MudSelectItem Value="@role">@role</MudSelectItem>
}
</MudSelect>
</MudTd>
<MudTd DataLabel="Message">
<MudTextField @bind-Value="context.Entry" Required />
</MudTd>
<MudTd style="text-align: center">
<MudIconButton Icon="@Icons.Material.Filled.Add"
Color="Color.Success"
Size="Size.Small"
OnClick="@(() => AddNewMessageBelow(context))"
/>
<MudIconButton Icon="@Icons.Material.Filled.Delete"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => RemoveMessage(context))"
/>
</MudTd>
</RowEditingTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
</MudForm>
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">
@T("Cancel")
</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
@if(this.IsEditing)
{
@T("Update")
}
else
{
@T("Add")
}
</MudButton>
</DialogActions>
</MudDialog>

View File

@ -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!;
/// <summary>
/// The chat template's number in the list.
/// </summary>
[Parameter]
public uint DataNum { get; set; }
/// <summary>
/// The chat template's ID.
/// </summary>
[Parameter]
public string DataId { get; set; } = Guid.NewGuid().ToString();
/// <summary>
/// The chat template name chosen by the user.
/// </summary>
[Parameter]
public string DataName { get; set; } = string.Empty;
/// <summary>
/// What is the system prompt?
/// </summary>
[Parameter]
public string DataSystemPrompt { get; set; } = string.Empty;
/// <summary>
/// Should the dialog be in editing mode?
/// </summary>
[Parameter]
public bool IsEditing { get; init; }
[Parameter]
public List<EntryItem> AdditionalMessages { get; set; } = [];
[Inject]
private ILogger<ProviderDialog> Logger { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
/// The list of used chat template 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;
private EntryItem messageEntryBeforeEdit;
private readonly List<EntryItem> additionalMessagesEntries = [];
private readonly List<string> 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();
}

View File

@ -16,6 +16,7 @@
} }
<SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders"/> <SettingsPanelProfiles AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
<SettingsPanelChatTemplates AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
<SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders"/> <SettingsPanelApp AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
<SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders"/> <SettingsPanelChat AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>
<SettingsPanelWorkspaces AvailableLLMProvidersFunc="() => this.availableLLMProviders"/> <SettingsPanelWorkspaces AvailableLLMProvidersFunc="() => this.availableLLMProviders"/>

View File

@ -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
/// <summary>
/// Returns a string that represents the profile in a human-readable format.
/// </summary>
/// <returns>A string that represents the profile in a human-readable format.</returns>
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}
""";
}
}

View File

@ -36,6 +36,11 @@ public sealed class Data
/// </summary> /// </summary>
public List<Profile> Profiles { get; init; } = []; public List<Profile> Profiles { get; init; } = [];
/// <summary>
/// List of configured chat templates.
/// </summary>
public List<ChatTemplate> ChatTemplates { get; init; } = [];
/// <summary> /// <summary>
/// List of enabled plugins. /// List of enabled plugins.
/// </summary> /// </summary>
@ -61,6 +66,11 @@ public sealed class Data
/// </summary> /// </summary>
public uint NextProfileNum { get; set; } = 1; public uint NextProfileNum { get; set; } = 1;
/// <summary>
/// The next chat template number to use.
/// </summary>
public uint NextChatTemplateNum { get; set; } = 1;
public DataApp App { get; init; } = new(); public DataApp App { get; init; } = new();
public DataChat Chat { get; init; } = new(); public DataChat Chat { get; init; } = new();