mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-17 22:37:59 +00:00
Locked changes to a chat while an answer is being written (#1058)
This commit is contained in:
1 parent
88d1f4d96a
commit
fbb65fd72d
19 files changed
+841
-59
No files matched your search
@@ -28,7 +28,7 @@
|
||||
RegenerateFunc="@this.RegenerateBlock"
|
||||
RegenerateEnabled="@(() => this.IsProviderSelected && this.ChatThread.IsLLMProviderAllowed(this.Provider))"
|
||||
RollbackFunc="@this.RollbackBlock"
|
||||
RollbackEnabled="@(() => !this.IsCurrentChatStreaming)"
|
||||
BlockActionsEnabled="@(() => this.blockActions.AreAllowed(this.ChatThread))"
|
||||
EditLastBlockFunc="@this.EditLastBlock"
|
||||
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<ChatTemplateSelection MarginLeft="" CanChatThreadBeUsedForTemplate="@this.CanThreadBeSaved" CurrentChatThread="@this.ChatThread" CurrentChatTemplate="@this.currentChatTemplate" CurrentChatTemplateChanged="@this.ChatTemplateWasChanged"/>
|
||||
<ChatTemplateSelection MarginLeft="" CanChatThreadBeUsedForTemplate="@this.CanThreadBecomeTemplate" CurrentChatThread="@this.ChatThread" CurrentChatTemplate="@this.currentChatTemplate" CurrentChatTemplateChanged="@this.ChatTemplateWasChanged"/>
|
||||
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
@@ -109,7 +109,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
<AttachDocuments Name="File Attachments" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" OwnerChat="@this.ChatThread" EnsureOwnerChatAsync="@this.EnsureMediaImportChatAsync" Disabled="@this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner)"/>
|
||||
<AttachDocuments Name="File Attachments" DocumentPaths="@this.ComposerState.FileAttachments" DocumentPathsChanged="@this.ComposerAttachmentsChanged" CatchAllDocuments="true" UseSmallForm="true" ShowMediaStatus="false" Provider="@this.Provider" OwnerChat="@this.ChatThread" EnsureOwnerChatAsync="@this.EnsureMediaImportChatAsync" Disabled="@(!this.CanAttachFiles)"/>
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
|
||||
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
|
||||
|
||||
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
|
||||
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.CanProfileBeChanged)" DisabledText="@this.ProfileDisabledText"/>
|
||||
|
||||
@if (this.SettingsManager.AreToolsEnabled())
|
||||
{
|
||||
@@ -140,7 +140,7 @@
|
||||
|
||||
@if (PreviewFeatures.PRE_RAG_2024.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@(async options => await this.SetCurrentDataSourceOptions(options))" DataSourcesAISelected="@this.GetAgentSelectedDataSources()"/>
|
||||
<DataSourceSelection @ref="@this.dataSourceSelectionComponent" PopoverTriggerMode="PopoverTriggerMode.ICON" LLMProvider="@this.Provider" DataSourceOptions="@this.GetCurrentDataSourceOptions()" DataSourceOptionsChanged="@this.DataSourceOptionsWereChanged" DataSourcesAISelected="@this.GetAgentSelectedDataSources()" ReadOnly="@this.IsCurrentChatStreaming" ReadOnlyMessage="@T("Data source changes are locked while a response is running. Your current selection is shown below and applies again from the next message once the run is finished.")"/>
|
||||
}
|
||||
|
||||
@if (this.SettingsManager.ConfigurationData.Confidence.ShowProviderConfidence)
|
||||
|
||||
@@ -39,6 +39,17 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<AIStudio.Settings.Provider> ProviderChanged { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tells whether an answer is being written for the open chat, whenever that changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For the parts of the chat page outside this component, such as the provider selection. The AI
|
||||
/// job reports every part of an answer, and this component renders after each report. The page
|
||||
/// learns only when an answer starts or ends, see OnAfterRenderAsync.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public EventCallback<bool> IsAnswerBeingWrittenChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Action<string> WorkspaceName { get; set; } = _ => { };
|
||||
@@ -91,6 +102,7 @@ public partial class ChatComponent : MSGComponentBase
|
||||
private bool autoSaveEnabled;
|
||||
private HashSet<string> selectedToolIds = [];
|
||||
private bool previousInputForbidden = true;
|
||||
private bool announcedAnswerBeingWritten;
|
||||
private Guid lastSeenChatId = Guid.Empty;
|
||||
private AIStudio.Settings.Provider lastSeenProvider = AIStudio.Settings.Provider.NONE;
|
||||
private string currentWorkspaceName = string.Empty;
|
||||
@@ -154,6 +166,11 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// </summary>
|
||||
private ChatDraftAutosave? draftAutosave;
|
||||
|
||||
/// <summary>
|
||||
/// The only way to change the messages of the open chat, which is locked while an answer is being written.
|
||||
/// </summary>
|
||||
private ChatBlockActions blockActions = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A new chat which has not reached the disk yet, or null.
|
||||
/// </summary>
|
||||
@@ -293,6 +310,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
{
|
||||
this.MediaTranscriptionService.StateChanged += this.OnMediaImportStateChanged;
|
||||
|
||||
// Created before anything is awaited, because the first render may happen there already:
|
||||
this.blockActions = new(chatId => this.AIJobService.IsChatGenerationActive(chatId));
|
||||
|
||||
//
|
||||
// Leaving the chat page takes the composer along. Registered before anything is awaited, so
|
||||
// that a component which goes away while it starts can never leave its handler behind:
|
||||
@@ -568,6 +588,20 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
this.previousInputForbidden = inputForbidden;
|
||||
|
||||
//
|
||||
// The page hears about an answer starting or ending here rather than with every report of the
|
||||
// AI job, since a render of the page redraws the workspace tree. This component renders after
|
||||
// every such report and after another chat was opened, so the page never misses a change. The
|
||||
// first render reports in any case: the page may still hold what another instance of this
|
||||
// component told it, e.g., before the workspaces were shown or hidden.
|
||||
//
|
||||
var isAnswerBeingWritten = this.IsCurrentChatStreaming;
|
||||
if (firstRender || isAnswerBeingWritten != this.announcedAnswerBeingWritten)
|
||||
{
|
||||
this.announcedAnswerBeingWritten = isAnswerBeingWritten;
|
||||
await this.IsAnswerBeingWrittenChanged.InvokeAsync(isAnswerBeingWritten);
|
||||
}
|
||||
|
||||
//
|
||||
// Everything which can move the token count also renders this component: the selections in
|
||||
// the toolbar, the attachments and the composer all travel through an event callback whose
|
||||
@@ -755,6 +789,35 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private bool CanThreadBeCopied => this.CanThreadBeSaved && !this.IsCurrentChatStreaming && !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a chat template can be made from the open chat. While an answer is being written, the template would copy it unfinished.
|
||||
/// </summary>
|
||||
private bool CanThreadBecomeTemplate => this.CanThreadBeSaved && !this.IsCurrentChatStreaming;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user may pick another profile, which the open chat carries from then on.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Picking a profile replaces the thread with a copy which carries it. While an answer is being
|
||||
/// written, the next update of the AI job puts the thread of the job back in place of that copy,
|
||||
/// and the choice is lost.
|
||||
/// </remarks>
|
||||
private bool CanProfileBeChanged => this.currentChatTemplate.AllowProfileUsage && !this.IsCurrentChatStreaming;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user may change the attachments of the message being written.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Not while an answer is being written, just like the text of the message, and not while the
|
||||
/// audio or video files of the chat are being transcribed.
|
||||
/// </remarks>
|
||||
private bool CanAttachFiles => !this.IsCurrentChatStreaming && !this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner);
|
||||
|
||||
// The chat template locks the profile for good, an answer being written only until it is complete:
|
||||
private string ProfileDisabledText => this.currentChatTemplate.AllowProfileUsage
|
||||
? T("Profile changes are locked while a response is running.")
|
||||
: T("Profile usage is disabled according to your chat template settings.");
|
||||
|
||||
/// <summary>
|
||||
/// Whether the open chat can move into a workspace, which is exactly when the workspace tree offers it.
|
||||
/// </summary>
|
||||
@@ -916,6 +979,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task ProfileWasChanged(Profile profile)
|
||||
{
|
||||
if (!this.CanProfileBeChanged)
|
||||
return;
|
||||
|
||||
this.currentProfile = this.SettingsManager.GetProfileById(profile.Id);
|
||||
|
||||
//
|
||||
@@ -1014,6 +1080,22 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
return this.earlyDataSourceOptions;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes over what the user changed in the data source selection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// While an answer is being written, the selection is read-only and turns every change down
|
||||
/// itself, since the answer reads the very options it shows. Checked here once more, so that the
|
||||
/// chat never stores a change which reached it anyway.
|
||||
/// </remarks>
|
||||
private async Task DataSourceOptionsWereChanged(DataSourceOptions updatedOptions)
|
||||
{
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return;
|
||||
|
||||
await this.SetCurrentDataSourceOptions(updatedOptions);
|
||||
}
|
||||
|
||||
private async Task SetCurrentDataSourceOptions(DataSourceOptions updatedOptions)
|
||||
{
|
||||
@@ -1152,7 +1234,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
/// message lives in the composer only. It becomes a disappearing chat of its own then. Starting a
|
||||
/// new chat in a workspace creates the thread right away instead, see unstoredChat. That one is
|
||||
/// stored with whatever its draft holds, and also while a transcript is still being made for it,
|
||||
/// since the transcript would otherwise arrive in a chat which is nowhere.<br/><br/>
|
||||
/// since the transcript would otherwise arrive in a chat which is nowhere. Only when its workspace
|
||||
/// was deleted meanwhile, the chat goes along with it.<br/><br/>
|
||||
///
|
||||
/// The composer is cleared before the chat is stored, since the message has moved into its chat.
|
||||
/// Leaving twice in a row, e.g., with two quick clicks, therefore stores it only once.
|
||||
@@ -1179,7 +1262,13 @@ public partial class ChatComponent : MSGComponentBase
|
||||
this.ComposerState.Clear();
|
||||
}
|
||||
else if (unstoredChatBeingLeft is not null && (!unstoredChatBeingLeft.Draft.IsEmpty || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(unstoredChatBeingLeft.ChatId))))
|
||||
{
|
||||
// Deleting the workspace took the chat along. Storing it would bring the workspace back, as an unnamed one:
|
||||
if (!WorkspaceBehaviour.IsWorkspaceExisting(unstoredChatBeingLeft.WorkspaceId))
|
||||
return;
|
||||
|
||||
chat = unstoredChatBeingLeft;
|
||||
}
|
||||
else
|
||||
return;
|
||||
|
||||
@@ -1261,6 +1350,14 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task SendMessage(bool reuseLastUserPrompt = false)
|
||||
{
|
||||
//
|
||||
// One answer at a time per chat. Checked before anything touches the chat or the composer:
|
||||
// the AI job service would turn a second answer down only after the chat got its message and
|
||||
// an empty answer, which then stays.
|
||||
//
|
||||
if (this.IsCurrentChatStreaming)
|
||||
return;
|
||||
|
||||
if (this.MediaTranscriptionService.IsBusy(this.CurrentMediaImportOwner))
|
||||
return;
|
||||
|
||||
@@ -1794,10 +1891,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task RemoveBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
if (!this.blockActions.TryRemove(this.ChatThread, block))
|
||||
return;
|
||||
|
||||
this.ChatThread.Remove(block);
|
||||
|
||||
this.hasUnsavedChanges = true;
|
||||
await this.SaveThread();
|
||||
this.StateHasChanged();
|
||||
@@ -1805,13 +1901,12 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task RegenerateBlock(IContent aiBlock)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
return;
|
||||
|
||||
if(!this.ChatThread.IsLLMProviderAllowed(this.Provider))
|
||||
return;
|
||||
|
||||
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
|
||||
|
||||
if (!this.blockActions.TryRemoveForRegenerate(this.ChatThread, aiBlock))
|
||||
return;
|
||||
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
@@ -1820,11 +1915,8 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private async Task RollbackBlock(IContent aiBlock)
|
||||
{
|
||||
if (this.ChatThread is null || this.IsCurrentChatStreaming)
|
||||
return;
|
||||
|
||||
// Which parts of the thread a rollback resets and which it keeps is documented at RollBackTo:
|
||||
if (!this.ChatThread.RollBackTo(aiBlock))
|
||||
if (!this.blockActions.TryRollBack(this.ChatThread, aiBlock))
|
||||
return;
|
||||
|
||||
// The rollback reset the AI-selected data sources, which the data source selection still shows:
|
||||
@@ -1838,19 +1930,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private Task EditLastUserBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
if (!this.blockActions.TryEditLastUserBlock(this.ChatThread, block))
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (block is not ContentText)
|
||||
return Task.CompletedTask;
|
||||
|
||||
var lastBlock = this.ChatThread.Blocks.Last();
|
||||
var lastBlockContent = lastBlock.Content;
|
||||
if(lastBlockContent is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.ChatThread.RemoveForEditing(block);
|
||||
this.ChatThread.Remove(lastBlockContent);
|
||||
this.RestoreComposerForEditing();
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
@@ -1859,13 +1941,9 @@ public partial class ChatComponent : MSGComponentBase
|
||||
|
||||
private Task EditLastBlock(IContent block)
|
||||
{
|
||||
if(this.ChatThread is null)
|
||||
if (!this.blockActions.TryEditLastBlock(this.ChatThread, block))
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (block is not ContentText)
|
||||
return Task.CompletedTask;
|
||||
|
||||
this.ChatThread.RemoveForEditing(block);
|
||||
this.RestoreComposerForEditing();
|
||||
this.hasUnsavedChanges = true;
|
||||
this.StateHasChanged();
|
||||
|
||||
@@ -64,6 +64,9 @@ public partial class ChatTemplateSelection : MSGComponentBase
|
||||
|
||||
private async Task CreateNewChatTemplateFromChat()
|
||||
{
|
||||
if (!this.CanChatThreadBeUsedForTemplate)
|
||||
return;
|
||||
|
||||
var dialogParameters = new DialogParameters<SettingsDialogChatTemplate>
|
||||
{
|
||||
{ x => x.CreateTemplateFromExistingChatThread, true },
|
||||
|
||||
@@ -57,7 +57,14 @@
|
||||
}
|
||||
else if (this.showDataSourceSelection)
|
||||
{
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Dense="@true"/>
|
||||
@if (this.ReadOnly && !string.IsNullOrWhiteSpace(this.ReadOnlyMessage))
|
||||
{
|
||||
<MudAlert Dense="@true" Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-2">
|
||||
@this.ReadOnlyMessage
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudTextSwitch Label="@T("Are data sources enabled?")" Value="@this.areDataSourcesEnabled" LabelOn="@T("Yes, I want to use data sources.")" LabelOff="@T("No, I don't want to use data sources.")" ValueChanged="@this.EnabledChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
@if (this.areDataSourcesEnabled)
|
||||
{
|
||||
@* Where Semantic Search cannot be used, there is no choice to make, only the reason to tell: *@
|
||||
@@ -69,15 +76,15 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextSwitch Label="@T("Semantic Search")" Value="@this.IsSemanticSearchPreferred" LabelOn="@T("Yes, the AI searches your data sources itself, whenever a question calls for it (Semantic Search).")" LabelOff="@T("No, AI Studio searches your data sources with every message, before the AI answers (classic RAG).")" ValueChanged="@this.SemanticSearchChanged" Dense="@true"/>
|
||||
<MudTextSwitch Label="@T("Semantic Search")" Value="@this.IsSemanticSearchPreferred" LabelOn="@T("Yes, the AI searches your data sources itself, whenever a question calls for it (Semantic Search).")" LabelOff="@T("No, AI Studio searches your data sources with every message, before the AI answers (classic RAG).")" ValueChanged="@this.SemanticSearchChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
}
|
||||
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Dense="@true"/>
|
||||
<MudTextSwitch Label="@T("AI-based data source selection")" Value="@this.aiBasedSourceSelection" LabelOn="@T("Yes, let the AI decide which data sources are needed.")" LabelOff="@T("No, I manually decide which data source to use.")" ValueChanged="@this.AutoModeChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
|
||||
@* With Semantic Search, no agent takes part: nothing would validate the retrieved data. *@
|
||||
@if (this.SettingsManager.ConfigurationData.AgentRetrievalContextValidation.EnableRetrievalContextValidation && !this.IsSemanticSearchEffective)
|
||||
{
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Dense="@true"/>
|
||||
<MudTextSwitch Label="@T("AI-based data validation")" Value="@this.aiBasedValidation" LabelOn="@T("Yes, let the AI validate & filter the retrieved data.")" LabelOff="@T("No, use all data retrieved from the data sources.")" ValueChanged="@this.ValidationModeChanged" Dense="@true" Disabled="@this.ReadOnly"/>
|
||||
}
|
||||
|
||||
@switch (this.aiBasedSourceSelection)
|
||||
@@ -95,8 +102,8 @@
|
||||
break;
|
||||
|
||||
case false:
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-2" Disabled="@this.aiBasedSourceSelection">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@this.SelectionChanged" Style="max-height: 14em;">
|
||||
<MudField Label="@T("Available Data Sources")" Variant="Variant.Outlined" Class="mb-2" Disabled="@(this.aiBasedSourceSelection || this.ReadOnly)">
|
||||
<MudList T="IDataSource" Dense="@true" Class="data-source-rows" SelectionMode="@this.GetListSelectionMode()" @bind-SelectedValues:get="@this.selectedDataSources" @bind-SelectedValues:set="@this.SelectionChanged" ReadOnly="@this.ReadOnly" Style="max-height: 14em;">
|
||||
@foreach (var source in this.GetListedDataSources())
|
||||
{
|
||||
<DataSourceSelectionRow DataSource="@source" BlockReason="@this.GetBlockReason(source)"/>
|
||||
|
||||
@@ -45,11 +45,21 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For options somebody else decided on, such as those of a chat template an organization
|
||||
/// rolled out. Seeing which data such a chat will search is the point; changing it here is not.
|
||||
/// rolled out. Seeing which data such a chat will search is the point; changing it here is not.<br/><br/>
|
||||
///
|
||||
/// Also for options which must not change right now, such as those of a chat while an answer is
|
||||
/// being written for it: the answer reads them, and they are the very options shown here, so a
|
||||
/// change would reach it halfway. Every change is turned down before it touches the options.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool ReadOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Tells why the options cannot be changed, shown at the top of the popover while they are read-only.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string ReadOnlyMessage { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the options edited here are the data source defaults of the chat.
|
||||
/// </summary>
|
||||
@@ -324,6 +334,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
private async Task EnabledChanged(bool state)
|
||||
{
|
||||
if (this.ReadOnly)
|
||||
return;
|
||||
|
||||
this.areDataSourcesEnabled = state;
|
||||
this.DataSourceOptions.DisableDataSources = !this.areDataSourcesEnabled;
|
||||
|
||||
@@ -334,6 +347,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
private async Task AutoModeChanged(bool state)
|
||||
{
|
||||
if (this.ReadOnly)
|
||||
return;
|
||||
|
||||
this.aiBasedSourceSelection = state;
|
||||
this.DataSourceOptions.AutomaticDataSourceSelection = this.aiBasedSourceSelection;
|
||||
|
||||
@@ -343,6 +359,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
private async Task SemanticSearchChanged(bool state)
|
||||
{
|
||||
if (this.ReadOnly)
|
||||
return;
|
||||
|
||||
this.retrievalMode = state ? DataSourceRetrievalMode.SEMANTIC_SEARCH : DataSourceRetrievalMode.EVERY_MESSAGE;
|
||||
this.DataSourceOptions.RetrievalMode = this.retrievalMode;
|
||||
|
||||
@@ -384,6 +403,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
private async Task ValidationModeChanged(bool state)
|
||||
{
|
||||
if (this.ReadOnly)
|
||||
return;
|
||||
|
||||
this.aiBasedValidation = state;
|
||||
this.DataSourceOptions.AutomaticValidation = this.aiBasedValidation;
|
||||
|
||||
@@ -393,6 +415,9 @@ public partial class DataSourceSelection : MSGComponentBase
|
||||
|
||||
private async Task SelectionChanged(IReadOnlyCollection<IDataSource>? chosenDataSources)
|
||||
{
|
||||
if (this.ReadOnly)
|
||||
return;
|
||||
|
||||
this.selectedDataSources = chosenDataSources ?? [];
|
||||
|
||||
//
|
||||
|
||||
@@ -34,6 +34,10 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
/// <summary>
|
||||
/// Gets or sets whether provider selection is disabled.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A choice which reaches a disabled selection anyway is turned down. Keeping the selected provider
|
||||
/// up to date after its settings changed is no choice, so that goes on.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
@@ -55,6 +59,9 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
|
||||
private async Task SelectionChanged(AIStudio.Settings.Provider provider)
|
||||
{
|
||||
if (this.Disabled)
|
||||
return;
|
||||
|
||||
this.ProviderSettings = provider;
|
||||
await this.ProviderSettingsChanged.InvokeAsync(provider);
|
||||
}
|
||||
|
||||
@@ -382,11 +382,15 @@ public partial class Workspaces : MSGComponentBase
|
||||
|
||||
private bool IsChatTreeItemBusy(TreeItemData treeItem)
|
||||
{
|
||||
return treeItem.Type is TreeItemType.CHAT
|
||||
&& (this.AIJobService.IsChatGenerationActive(treeItem.ChatId)
|
||||
|| this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(treeItem.ChatId)));
|
||||
return treeItem.Type is TreeItemType.CHAT && this.IsChatBusy(treeItem.ChatId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether an answer is being written for the given chat, or a transcript is being made for it.
|
||||
/// </summary>
|
||||
/// <param name="chatId">The chat to check.</param>
|
||||
private bool IsChatBusy(Guid chatId) => this.AIJobService.IsChatGenerationActive(chatId) || this.MediaTranscriptionService.IsBusy(MediaImportOwner.ForChat(chatId));
|
||||
|
||||
private string GetChatTreeItemTextStyle(TreeItemData treeItem)
|
||||
{
|
||||
var status = this.MediaTranscriptionService.GetSnapshot(MediaImportOwner.ForChat(treeItem.ChatId))?.Status;
|
||||
@@ -922,6 +926,18 @@ public partial class Workspaces : MSGComponentBase
|
||||
var workspaceId = Guid.Parse(Path.GetFileName(workspacePath));
|
||||
var workspaceName = await WorkspaceBehaviour.LoadWorkspaceNameAsync(workspaceId);
|
||||
|
||||
//
|
||||
// Deleting a workspace while one of its chats is being worked on would pull the ground from
|
||||
// under that work. The button cannot tell, since the chats of a workspace may not be loaded
|
||||
// yet, so we check before asking: nobody should confirm something that cannot happen anyway.
|
||||
//
|
||||
var chatIds = this.ReadChatIdsOfWorkspace(workspaceId);
|
||||
if (chatIds.Any(this.IsChatBusy))
|
||||
{
|
||||
await this.WarnAboutBusyWorkspaceAsync(workspaceName);
|
||||
return;
|
||||
}
|
||||
|
||||
var chatCount = Directory.EnumerateDirectories(workspacePath).Count();
|
||||
var dialogParameters = new DialogParameters<ConfirmDialog>
|
||||
{
|
||||
@@ -933,10 +949,43 @@ public partial class Workspaces : MSGComponentBase
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
Directory.Delete(workspacePath, true);
|
||||
await WorkspaceBehaviour.RemoveWorkspaceFromCacheAsync(workspaceId);
|
||||
if (!await WorkspaceBehaviour.DeleteWorkspaceAsync(workspaceId, this.IsChatBusy))
|
||||
{
|
||||
await this.WarnAboutBusyWorkspaceAsync(workspaceName);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var chatId in chatIds)
|
||||
this.MediaTranscriptionService.ClearOwnerState(MediaImportOwner.ForChat(chatId));
|
||||
|
||||
await this.LoadTreeItemsAsync(startPrefetch: false);
|
||||
|
||||
// The open chat went along with its workspace. Left on the screen, it would be stored again sooner or later:
|
||||
if (this.CurrentChatThread?.WorkspaceId == workspaceId)
|
||||
{
|
||||
this.CurrentChatThread = null;
|
||||
await this.CurrentChatThreadChanged.InvokeAsync(this.CurrentChatThread);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the chats of the given workspace, together with the open chat when it belongs there.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A new chat started in a workspace reaches the disk only with its first message. Until then, it
|
||||
/// is missing from the workspace on the disk, but it may already wait for a transcript.
|
||||
/// </remarks>
|
||||
/// <param name="workspaceId">The workspace to read.</param>
|
||||
private HashSet<Guid> ReadChatIdsOfWorkspace(Guid workspaceId)
|
||||
{
|
||||
var chatIds = WorkspaceBehaviour.ReadChatIdsOfWorkspace(workspaceId).ToHashSet();
|
||||
if (this.CurrentChatThread is { } openChat && openChat.WorkspaceId == workspaceId)
|
||||
chatIds.Add(openChat.ChatId);
|
||||
|
||||
return chatIds;
|
||||
}
|
||||
|
||||
private Task WarnAboutBusyWorkspaceAsync(string workspaceName) => MessageBus.INSTANCE.SendWarning(new(Icons.Material.Filled.FolderDelete, string.Format(T("The workspace '{0}' cannot be deleted while one of its chats is being answered or transcribed. Please try again once that is finished."), workspaceName)));
|
||||
|
||||
/// <summary>
|
||||
/// Moves the given chat into another workspace and shows it there in the tree.
|
||||
|
||||
Reference in new issue
Block a user