mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 13:07:21 +00:00
Added profile, provider, and workspace preselection to chat templates (#1099)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
ab9df99cf0
commit
24c4c31035
34 files changed
+2084
-142
No files matched your search
@@ -901,6 +901,84 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.selectedToolIds = await this.ToolRegistry.FilterSelectableToolIdsAsync(Tools.Components.CHAT, templateToolIds);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes over the profile and provider which the chat template preselects, wherever it preselects one.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A preselection, not a limit — the user changes both in the chat as usual. A template which
|
||||
/// says nothing, or names a provider which is gone or may not be used for chats, leaves the
|
||||
/// current choice in place, so the chat starts as it would without the template. A template
|
||||
/// whose profile is gone starts with the default profile of the chat settings instead. What the
|
||||
/// previous template preselected is not carried over, see SettingsManager.ChooseChatTemplateProfile.
|
||||
/// The caller tells the parent about a new provider.
|
||||
/// </remarks>
|
||||
/// <param name="previousChatTemplate">The chat template the current profile and provider were chosen with; the current one when the template does not change.</param>
|
||||
/// <returns>True when the provider changed.</returns>
|
||||
private bool ApplyChatTemplateProfileAndProvider(ChatTemplate previousChatTemplate)
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.ChooseChatTemplateProfile(this.currentChatTemplate, previousChatTemplate, this.currentProfile);
|
||||
|
||||
var provider = this.SettingsManager.ChooseChatTemplateProvider(this.currentChatTemplate, previousChatTemplate, this.Provider);
|
||||
if (provider == this.Provider)
|
||||
return false;
|
||||
|
||||
this.Provider = provider;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The workspace a chat which has no thread yet gets with its first content.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first content is the first message, a media import, or a draft kept when the user leaves
|
||||
/// the page. A chat without a thread belongs to no workspace, so the workspace of its chat
|
||||
/// template fills in, opened or created by its name. That happens only here, when the chat gets
|
||||
/// something to keep: merely opening the chat page or trying out a template creates no
|
||||
/// workspace, and a workspace the user deleted stays gone until a chat with that template
|
||||
/// actually starts. Without a usable workspace of the template, the chat stays without one.
|
||||
/// </remarks>
|
||||
/// <returns>The ID of the workspace, or Guid.Empty for none.</returns>
|
||||
private async Task<Guid> ResolveWorkspaceOfFirstThreadAsync()
|
||||
{
|
||||
if (this.currentWorkspaceId != Guid.Empty)
|
||||
return this.currentWorkspaceId;
|
||||
|
||||
if (this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not { } workspaceName)
|
||||
return Guid.Empty;
|
||||
|
||||
return await WorkspaceBehaviour.ResolveOrCreateChatTemplateWorkspaceAsync(workspaceName, this.currentChatTemplate.GetSafeName());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hands a thread which this component just created for a chat without one to the parent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The thread is a parameter of this component, and the parent hands over its own whenever it
|
||||
/// renders. So the parent hears about the new thread before anything else may render it, see
|
||||
/// StartNewChat. Only afterward, the header shows the workspace of the thread, since showing a
|
||||
/// workspace name renders the parent as well.
|
||||
/// </remarks>
|
||||
private async Task PublishNewChatThreadAsync()
|
||||
{
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
|
||||
// The header shows the workspace of the new thread from now on, e.g., the one of its chat template:
|
||||
if (this.ChatThread is not null && this.ChatThread.WorkspaceId != Guid.Empty)
|
||||
await this.SyncWorkspaceHeaderWithChatThreadAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the open chat is in the workspace which the given chat template names.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a workspace was the choice of the template rather than of the user, so a chat started
|
||||
/// with another template does not have to stay there.
|
||||
/// </remarks>
|
||||
private bool IsInWorkspaceOfChatTemplate(ChatTemplate chatTemplate) => this.currentWorkspaceId != Guid.Empty
|
||||
&& this.SettingsManager.GetChatTemplateWorkspaceName(chatTemplate) is { } workspaceName
|
||||
&& string.Equals(WorkspaceBehaviour.NormalizeWorkspaceName(this.currentWorkspaceName), workspaceName, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// The data source options a chat starts with: those of its chat template, or the chat defaults.
|
||||
/// </summary>
|
||||
@@ -1006,24 +1084,36 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task ChatTemplateWasChanged(ChatTemplate chatTemplate)
|
||||
{
|
||||
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
|
||||
var nextChatTemplate = this.SettingsManager.GetChatTemplateById(chatTemplate.Id);
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
//
|
||||
// Starting the new chat is what hands the new template to it, with its text, attachments,
|
||||
// and selections. Nothing changes here before, since the user may still decline to leave
|
||||
// the open chat; it then stays as it was, with its template. The new chat stays in the
|
||||
// workspace of the open one, unless that workspace is merely the one of the current
|
||||
// template: then the new template decides where its chat goes.
|
||||
//
|
||||
await this.StartNewChat(useSameWorkspace: !this.IsInWorkspaceOfChatTemplate(this.currentChatTemplate), chatTemplate: nextChatTemplate);
|
||||
return;
|
||||
}
|
||||
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
this.currentChatTemplate = nextChatTemplate;
|
||||
if(!string.IsNullOrWhiteSpace(this.currentChatTemplate.PredefinedUserPrompt))
|
||||
this.ComposerState.SetSystemInput(this.currentChatTemplate.PredefinedUserPrompt);
|
||||
|
||||
// Apply template's file attachments (replaces existing):
|
||||
this.ComposerState.ReplaceFileAttachments(this.currentChatTemplate.FileAttachments);
|
||||
|
||||
if (this.ChatThread is not null)
|
||||
{
|
||||
// Starting the new chat is what hands the selection of the new template to it:
|
||||
await this.StartNewChat(true);
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Without a thread there is nothing to start anew, so the selection of the new template is
|
||||
// applied right here. It travels into the thread which the first message creates.
|
||||
// applied right here. It travels into the thread which the first message creates, and that
|
||||
// message also decides the workspace, see ResolveWorkspaceOfFirstThreadAsync.
|
||||
//
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
await this.ApplyChatTemplateToolSelectionAsync();
|
||||
this.ApplyAutomaticDataSourceOptions();
|
||||
}
|
||||
@@ -1047,6 +1137,19 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
|
||||
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
|
||||
|
||||
//
|
||||
// A chat which has not started yet keeps up with its chat template. The provider follows
|
||||
// only when the template now preselects another one, so that a provider the user picked
|
||||
// by hand survives a change of some unrelated setting:
|
||||
//
|
||||
if (previousChatTemplate.Id != this.currentChatTemplate.Id || previousChatTemplate.ProviderId != this.currentChatTemplate.ProviderId)
|
||||
{
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
}
|
||||
else if (this.SettingsManager.GetChatTemplateStartProfile(this.currentChatTemplate) is { } templateProfile)
|
||||
this.currentProfile = templateProfile;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1256,10 +1359,16 @@ public partial class ChatComponent : MSGComponentBase
|
||||
if (!this.ComposerState.HasVisibleUserDraft)
|
||||
return;
|
||||
|
||||
//
|
||||
// The message moves out of the composer before anything below waits, so that leaving a
|
||||
// second time meanwhile finds nothing left to keep. Only the workspace follows afterward,
|
||||
// since finding or creating the one of the chat template may take a moment:
|
||||
//
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput));
|
||||
chat = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), Guid.Empty);
|
||||
this.MirrorComposerIntoDraft(chat);
|
||||
this.ComposerState.Clear();
|
||||
chat.WorkspaceId = await this.ResolveWorkspaceOfFirstThreadAsync();
|
||||
}
|
||||
else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId))))
|
||||
{
|
||||
@@ -1330,22 +1439,28 @@ public partial class ChatComponent : MSGComponentBase
|
||||
? string.Format(T("Transcription: {0}"), Path.GetFileName(firstMediaPath))
|
||||
: promptName;
|
||||
|
||||
this.ChatThread = this.NewChatThread(threadName);
|
||||
var chat = this.NewChatThread(threadName, await this.ResolveWorkspaceOfFirstThreadAsync());
|
||||
|
||||
// A message the user started belongs to the new chat, so it is stored with it:
|
||||
if (this.ComposerState.HasUserDraft)
|
||||
this.MirrorComposerIntoDraft(this.ChatThread);
|
||||
this.MirrorComposerIntoDraft(chat);
|
||||
|
||||
//
|
||||
// The parent hears about the new thread before storing it waits, as with the first message.
|
||||
// A render of the parent in between would otherwise hand back no thread at all, and the
|
||||
// transcript would arrive in a chat which is nowhere:
|
||||
//
|
||||
this.ChatThread = chat;
|
||||
await this.PublishNewChatThreadAsync();
|
||||
|
||||
//
|
||||
// Like saving, this goes through the workspace component whenever there is one, so its tree
|
||||
// lists the new chat right away, together with the progress of the transcription, instead
|
||||
// of only after the first message was sent:
|
||||
//
|
||||
await this.StoreAndListChatAsync(this.ChatThread);
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
await this.StoreAndListChatAsync(chat);
|
||||
await this.SyncForegroundChatAsync();
|
||||
return this.ChatThread;
|
||||
return chat;
|
||||
}
|
||||
|
||||
private async Task SendMessage(bool reuseLastUserPrompt = false)
|
||||
@@ -1377,9 +1492,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// Create a new chat thread if necessary:
|
||||
if (this.ChatThread is null)
|
||||
{
|
||||
this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput));
|
||||
this.MarkCurrentChatAsLoadedParameter();
|
||||
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
|
||||
this.ChatThread = this.NewChatThread(this.ExtractThreadName(this.ComposerState.UserInput), await this.ResolveWorkspaceOfFirstThreadAsync());
|
||||
await this.PublishNewChatThreadAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1576,12 +1690,17 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// Every way to start a new chat leads here: the buttons of the toolbar, a change of the chat
|
||||
/// template, and the workspace tree. So every new chat gets its chat template with the data
|
||||
/// sources, tools, and example conversation, its profile, and its provider in the same way, and
|
||||
/// the user is asked about unsaved changes in the same way.
|
||||
/// the user is asked about unsaved changes in the same way.<br/><br/>
|
||||
///
|
||||
/// A new chat template takes over only once the user agreed to leave the open chat and that
|
||||
/// chat is stored or deleted. Declining leaves the open chat as it was, with its template, and
|
||||
/// the template it runs with stays the one whose preselections the next chat must not carry over.
|
||||
/// </remarks>
|
||||
/// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message.</param>
|
||||
/// <param name="useSameWorkspace">True starts the new chat with a thread right away, in the workspace of the open chat. False starts it outside any workspace, and its thread comes with the first message. A chat which would belong to no workspace either way, while its chat template names one, also gets its thread only with the first message, in the workspace of the template.</param>
|
||||
/// <param name="deletePreviousChat">Whether to delete the open chat first.</param>
|
||||
/// <param name="targetWorkspaceId">The workspace for the new chat, when the workspace tree asks for one. When given, the new chat gets a thread in that workspace, whatever useSameWorkspace says.</param>
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null)
|
||||
/// <param name="chatTemplate">The chat template of the new chat, when the user picked another one. Without one, the new chat keeps the template of the open chat.</param>
|
||||
private async Task StartNewChat(bool useSameWorkspace = false, bool deletePreviousChat = false, Guid? targetWorkspaceId = null, ChatTemplate? chatTemplate = null)
|
||||
{
|
||||
//
|
||||
// Where the new chat goes is decided right away, when it is asked for. Deleting the open chat
|
||||
@@ -1589,6 +1708,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
var workspaceOfNewChat = targetWorkspaceId ?? this.currentWorkspaceId;
|
||||
|
||||
// The template the profile and the provider of the open chat were chosen with:
|
||||
var previousChatTemplate = this.currentChatTemplate;
|
||||
|
||||
//
|
||||
// Want the user to manage the chat storage manually? In that case, we have to ask the user
|
||||
// about possible data loss:
|
||||
@@ -1650,8 +1772,13 @@ public partial class ChatComponent : MSGComponentBase
|
||||
//
|
||||
this.hasUnsavedChanges = false;
|
||||
this.ComposerState.Clear();
|
||||
|
||||
// The open chat is left now, so the new template takes over:
|
||||
if (chatTemplate is not null)
|
||||
this.currentChatTemplate = chatTemplate;
|
||||
|
||||
this.RefreshCurrentProfileAndChatTemplate();
|
||||
|
||||
|
||||
//
|
||||
// Reset the LLM provider considering the user's settings:
|
||||
//
|
||||
@@ -1673,11 +1800,26 @@ public partial class ChatComponent : MSGComponentBase
|
||||
break;
|
||||
}
|
||||
|
||||
// What the chat template preselects comes last, so that it wins over the behavior above:
|
||||
if (this.ApplyChatTemplateProfileAndProvider(previousChatTemplate))
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
//
|
||||
// Reset the chat thread or create a new one. A target workspace always means a new chat in
|
||||
// that workspace, no matter what useSameWorkspace says:
|
||||
//
|
||||
var startsOutsideWorkspaces = !useSameWorkspace && targetWorkspaceId is null;
|
||||
|
||||
//
|
||||
// The workspace of the chat template fills in only where the new chat would belong to no
|
||||
// workspace otherwise. A workspace asked for on purpose wins: the one of the workspace list,
|
||||
// and the open one for the button that starts a new chat in it. Such a chat gets no thread
|
||||
// here: its first message creates one in that workspace, so that the workspace is created
|
||||
// only once there is something to keep, see ResolveWorkspaceOfFirstThreadAsync.
|
||||
//
|
||||
if (targetWorkspaceId is null && workspaceOfNewChat == Guid.Empty && this.SettingsManager.GetChatTemplateWorkspaceName(this.currentChatTemplate) is not null)
|
||||
startsOutsideWorkspaces = true;
|
||||
|
||||
if (startsOutsideWorkspaces)
|
||||
{
|
||||
//
|
||||
@@ -1916,7 +2058,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
var chatChatTemplate = this.ChatThread?.SelectedChatTemplate;
|
||||
|
||||
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider);
|
||||
|
||||
|
||||
// A chat without a thread has not started yet, so it starts with what its chat template preselects:
|
||||
if (this.ChatThread is null)
|
||||
this.ApplyChatTemplateProfileAndProvider(this.currentChatTemplate);
|
||||
|
||||
await this.ProviderChanged.InvokeAsync(this.Provider);
|
||||
|
||||
// Try to select the profile:
|
||||
@@ -2054,7 +2200,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
// that, and it may even bring an example conversation along -- reporting nothing for all
|
||||
// of it would tell a person their window is empty while their first message is not.
|
||||
//
|
||||
var thread = this.ChatThread ?? this.NewChatThread(string.Empty);
|
||||
var thread = this.ChatThread ?? this.NewChatThread(string.Empty, this.currentWorkspaceId);
|
||||
var toolDefinitions = this.GetRunnableToolDefinitions(offersSemanticSearch);
|
||||
provider = this.Provider;
|
||||
parts = ConversationParts.Of(thread, this.BuildSystemPromptFor(thread, toolDefinitions), this.UserInput, this.ComposerState.FileAttachments, provider.SupportsImageInput(), toolDefinitions);
|
||||
@@ -2153,18 +2299,22 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// The thread a new chat starts with, as the selections made so far decide it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// In one place because three code paths used to write it out, and because the token count has
|
||||
/// to measure the same thing they build. A count against a thread assembled differently from
|
||||
/// the one which is then sent would be wrong in exactly the moment a person looks at it: before
|
||||
/// they send their first message.
|
||||
/// The only place which builds a new thread: every way to start a chat comes here, and so does
|
||||
/// the token count, which has to measure the same thing they build. A count against a thread
|
||||
/// assembled differently from the one which is then sent would be wrong in exactly the moment a
|
||||
/// person looks at it: before they send their first message.
|
||||
///
|
||||
/// The workspace is no default of its own but always named by the caller, since that is the one
|
||||
/// part the ways to start a chat decide differently: StartNewChat decides it right away, and a
|
||||
/// chat which gets its thread with its first content asks ResolveWorkspaceOfFirstThreadAsync.
|
||||
///
|
||||
/// The data source options are those chosen before the chat had a thread. Starting a new chat in
|
||||
/// a workspace replaces them right afterward with those its chat template asks for.
|
||||
/// </remarks>
|
||||
/// <param name="name">The name of the thread.</param>
|
||||
/// <param name="workspaceId">The workspace of the thread. Without one, the thread goes to the workspace of the open chat.</param>
|
||||
/// <param name="workspaceId">The workspace of the thread, or Guid.Empty for none.</param>
|
||||
/// <returns>The new thread.</returns>
|
||||
private ChatThread NewChatThread(string name, Guid? workspaceId = null) => new()
|
||||
private ChatThread NewChatThread(string name, Guid workspaceId) => new()
|
||||
{
|
||||
DataSourceOptions = this.earlyDataSourceOptions,
|
||||
IncludeDateTime = true,
|
||||
@@ -2173,7 +2323,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
SelectedChatTemplate = this.currentChatTemplate.Id,
|
||||
SelectedToolIds = [..this.selectedToolIds],
|
||||
SystemPrompt = SystemPrompts.DEFAULT,
|
||||
WorkspaceId = workspaceId ?? this.currentWorkspaceId,
|
||||
WorkspaceId = workspaceId,
|
||||
ChatId = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Blocks = this.currentChatTemplate == ChatTemplate.NO_CHAT_TEMPLATE ? [] : this.currentChatTemplate.ExampleConversation.Select(block => block.DeepClone()).ToList(),
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
@using AIStudio.Settings.DataModel
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableWorkspaces.Count > 0)
|
||||
{
|
||||
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@T("Existing workspace (Optional)")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
|
||||
@foreach (var workspace in this.availableWorkspaces)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-1" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
|
||||
<WorkspaceNameField WorkspaceName="@this.WorkspaceName" WorkspaceNameChanged="@this.SetWorkspaceName" SelectLabel="@T("Existing workspace (Optional)")" Label="@T("Workspace name (Optional)")" HelperText="@T("Choose an existing workspace or enter a name that should be created when the launcher is opened.")" AdornmentIcon="@(this.OpensTemporaryChat ? Icons.Material.Filled.Timer : Icons.Material.Filled.CreateNewFolder)" AdornmentIconSize="Size.Small" TextFieldClass="mb-1"/>
|
||||
|
||||
@* The tile behaves differently with and without a workspace, so the form says which of the two is
|
||||
chosen right now instead of only explaining that both are possible. *@
|
||||
@@ -19,6 +10,12 @@
|
||||
? T("Without a workspace, the tile opens a disappearing chat: it belongs to no workspace and is deleted according to your workspace maintenance settings.")
|
||||
: T("The tile opens its chat in this workspace and creates the workspace when it does not exist yet."))
|
||||
</MudText>
|
||||
@if (this.OpensTemporaryChat && this.SettingsManager.GetChatTemplateWorkspaceName(this.SelectedChatTemplate) is { } templateWorkspaceName)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@string.Format(T("The chosen chat template keeps its chats in the workspace '{0}', so the tile opens its chat there instead. A workspace entered here wins over the one of the template."), templateWorkspaceName)
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ProviderId" ValueChanged="@this.SetProviderId" Label="@T("Chat provider")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.SmartToy">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
@foreach (var provider in this.SettingsManager.GetConfidentProviders(Components.CHAT))
|
||||
@@ -28,6 +25,12 @@
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.SelectedChatTemplate.ProviderId is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template preselects a provider of its own, and it wins over a selection made here as long as that provider can be used for chats.")
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ProfileId" ValueChanged="@this.SetProfileId" Label="@T("Chat profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no profile")</MudSelectItem>
|
||||
@@ -36,6 +39,12 @@
|
||||
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@if (this.SelectedChatTemplate is { AllowProfileUsage: true, ProfileId: not null })
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template preselects a profile of its own, and it wins over a selection made here as long as that profile exists.")
|
||||
</MudText>
|
||||
}
|
||||
<MudSelect T="string" Value="@this.ChatTemplateId" ValueChanged="@this.SetChatTemplateId" Label="@T("Chat template")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Article">
|
||||
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
|
||||
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no chat template")</MudSelectItem>
|
||||
@@ -44,20 +53,25 @@
|
||||
<MudSelectItem T="string" Value="@chatTemplate.Id">@chatTemplate.GetSafeName()</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE">
|
||||
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@dataSource.Id">@dataSource.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@* A chat template wins over what is chosen here, so the form says so before somebody picks
|
||||
something which would never take effect: *@
|
||||
@if (this.SelectedChatTemplate.DataSourceOptions is not null)
|
||||
@* Without the RAG preview, the chat uses no data sources, so there is nothing to choose here. A
|
||||
selection made before stays with the launcher and counts once RAG is switched on again: *@
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
|
||||
</MudText>
|
||||
<MudSelect T="string" Label="@T("Data sources (Optional)")" MultiSelection="@true" SelectedValues="@this.DataSourceIds" SelectedValuesChanged="@this.SetDataSourceIds" MultiSelectionTextFunc="@this.GetSelectedDataSourceText" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@AppIcons.DATABASE">
|
||||
@foreach (var dataSource in this.SettingsManager.ConfigurationData.DataSources)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@dataSource.Id">@dataSource.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@* A chat template wins over what is chosen here, so the form says so before somebody picks
|
||||
something which would never take effect: *@
|
||||
@if (this.SelectedChatTemplate.DataSourceOptions is not null)
|
||||
{
|
||||
<MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
|
||||
@T("The chosen chat template brings data sources of its own, and those win over a selection made here. Only a template can also leave the choice of sources to the AI, which is why it decides this on its own.")
|
||||
</MudText>
|
||||
}
|
||||
}
|
||||
|
||||
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
|
||||
|
||||
@@ -96,37 +96,6 @@ public partial class DirectChatLauncherForm : MSGComponentBase
|
||||
? this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT)
|
||||
: this.SettingsManager.GetChatTemplateById(this.ChatTemplateId);
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Configure the spellchecking for the workspace name input:
|
||||
this.SettingsManager.InjectSpellchecking(USER_INPUT_ATTRIBUTES);
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.availableWorkspaces = workspaceSnapshot.Workspaces;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//
|
||||
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
|
||||
// the user typed, though, so an empty selection is ignored:
|
||||
//
|
||||
private async Task SelectExistingWorkspace(string workspaceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
return;
|
||||
|
||||
await this.SetWorkspaceName(workspaceName);
|
||||
}
|
||||
|
||||
private async Task SetWorkspaceName(string workspaceName)
|
||||
{
|
||||
this.WorkspaceName = workspaceName;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@inherits MSGComponentBase
|
||||
|
||||
@if (this.availableWorkspaces.Count > 0 && !this.ReadOnly)
|
||||
{
|
||||
<MudSelect T="string" Value="@this.WorkspaceName" ValueChanged="@this.SelectExistingWorkspace" Strict="@false" Label="@this.SelectLabel" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.SelectClass" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Folder">
|
||||
@foreach (var workspace in this.availableWorkspaces)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@workspace.Name">@workspace.Name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
}
|
||||
|
||||
<MudTextField T="string" Text="@this.WorkspaceName" TextChanged="@this.SetWorkspaceName" Label="@this.Label" HelperText="@this.HelperText" Validation="@this.Validation" Immediate="@this.Immediate" ReadOnly="@this.ReadOnly" Variant="Variant.Outlined" Margin="Margin.Dense" Class="@this.TextFieldClass" Adornment="Adornment.Start" AdornmentIcon="@this.AdornmentIcon" IconSize="@this.AdornmentIconSize" UserAttributes="@SPELLCHECK_ATTRIBUTES"/>
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Names a workspace by its name: an existing one to pick from, or a new name to type.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Assistant chat launchers and chat templates both name their workspace this way, because a
|
||||
/// workspace has an ID of its own on every computer, and both create it when it does not exist
|
||||
/// yet. The field offers the existing workspaces only as a shortcut; the name it hands out is
|
||||
/// whatever the text field says. All texts come from the form around it, which knows what the
|
||||
/// name is used for.
|
||||
/// </remarks>
|
||||
public partial class WorkspaceNameField : MSGComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of the workspace. An empty name is up to the form around the field, e.g., a
|
||||
/// disappearing chat for an assistant chat launcher.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string WorkspaceName { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> WorkspaceNameChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The label of the selection of existing workspaces.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string SelectLabel { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The label of the text field for the name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Label { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string HelperText { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string AdornmentIcon { get; set; } = Icons.Material.Filled.CreateNewFolder;
|
||||
|
||||
[Parameter]
|
||||
public Size AdornmentIconSize { get; set; } = Size.Medium;
|
||||
|
||||
/// <summary>
|
||||
/// The validation of the name, or null for none.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<string, string?>? Validation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether every keystroke counts as a change, e.g., so that a validation follows the typing.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool Immediate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Shows the name without letting the user change it. The selection of existing workspaces
|
||||
/// is left out then, since picking from it would change the name.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string SelectClass { get; set; } = "mb-3 rounded-lg";
|
||||
|
||||
[Parameter]
|
||||
public string TextFieldClass { get; set; } = "mb-3";
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
this.SettingsManager.InjectSpellchecking(SPELLCHECK_ATTRIBUTES);
|
||||
await base.OnInitializedAsync();
|
||||
|
||||
var workspaceSnapshot = await WorkspaceBehaviour.GetOrLoadWorkspaceTreeShellAsync();
|
||||
this.availableWorkspaces = workspaceSnapshot.Workspaces;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//
|
||||
// Picking an existing workspace fills the name field. Clearing the select must not wipe a name
|
||||
// the user typed, though, so an empty selection is ignored:
|
||||
//
|
||||
private async Task SelectExistingWorkspace(string workspaceName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(workspaceName))
|
||||
return;
|
||||
|
||||
await this.SetWorkspaceName(workspaceName);
|
||||
}
|
||||
|
||||
private async Task SetWorkspaceName(string workspaceName)
|
||||
{
|
||||
this.WorkspaceName = workspaceName;
|
||||
await this.WorkspaceNameChanged.InvokeAsync(workspaceName);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user