mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-16 04:13:48 +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
@@ -0,0 +1,105 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// Changes the messages of a chat on behalf of the user, but never while an answer is being written for it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// While an answer is being written, the AI job holds the very same thread as the chat: it writes into
|
||||
/// the answer, and it stores the whole conversation every few seconds. A message the user removed in
|
||||
/// the meantime does not come back, but the job goes on writing an answer nobody sees, and the chat
|
||||
/// stays locked until that answer is complete. Removing the answer and sending another message would
|
||||
/// leave two answers being written for one chat. Changing older messages is no better: the job stores
|
||||
/// the conversation on another thread and could read it in the middle of the change.<br/><br/>
|
||||
///
|
||||
/// That is why every message is locked while an answer is being written, not only the answer itself.
|
||||
/// Stopping the answer unlocks the chat right away.
|
||||
/// </remarks>
|
||||
/// <param name="isAnswerBeingWritten">Whether an answer is being written for the chat with the given ID right now.</param>
|
||||
public sealed class ChatBlockActions(Func<Guid, bool> isAnswerBeingWritten)
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the user may change the messages of the given chat right now.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat, or null when there is none yet.</param>
|
||||
/// <returns>True when there is a chat and no answer is being written for it.</returns>
|
||||
public bool AreAllowed([NotNullWhen(true)] ChatThread? chat) => chat is not null && !isAnswerBeingWritten(chat.ChatId);
|
||||
|
||||
/// <summary>
|
||||
/// Removes a message, together with the hidden prompt an answer was written for.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat.</param>
|
||||
/// <param name="content">The content of the message to remove.</param>
|
||||
/// <returns>True when the message was removed.</returns>
|
||||
public bool TryRemove([NotNullWhen(true)] ChatThread? chat, IContent content)
|
||||
{
|
||||
if (!this.AreAllowed(chat) || !Holds(chat, content))
|
||||
return false;
|
||||
|
||||
chat.Remove(content);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes an answer so that it can be written anew for the same question.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat.</param>
|
||||
/// <param name="answer">The content of the answer to remove.</param>
|
||||
/// <returns>True when the answer was removed.</returns>
|
||||
public bool TryRemoveForRegenerate([NotNullWhen(true)] ChatThread? chat, IContent answer)
|
||||
{
|
||||
if (!this.AreAllowed(chat) || !Holds(chat, answer))
|
||||
return false;
|
||||
|
||||
chat.Remove(answer, removeForRegenerate: true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rolls the chat back, so that the given answer becomes its last message.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// What the rollback removes and what it keeps is documented at ChatThread.RollBackTo.
|
||||
/// </remarks>
|
||||
/// <param name="chat">The chat.</param>
|
||||
/// <param name="answer">The content of the answer to keep as the last message.</param>
|
||||
/// <returns>True when later messages were removed.</returns>
|
||||
public bool TryRollBack([NotNullWhen(true)] ChatThread? chat, IContent answer) => this.AreAllowed(chat) && chat.RollBackTo(answer);
|
||||
|
||||
/// <summary>
|
||||
/// Makes the last question the draft again, and removes the answer to it.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat.</param>
|
||||
/// <param name="question">The content of the question to edit.</param>
|
||||
/// <returns>True when the question became the draft.</returns>
|
||||
public bool TryEditLastUserBlock([NotNullWhen(true)] ChatThread? chat, IContent question)
|
||||
{
|
||||
if (!this.AreAllowed(chat) || question is not ContentText || !Holds(chat, question))
|
||||
return false;
|
||||
|
||||
var lastContent = chat.Blocks.Last().Content;
|
||||
chat.RemoveForEditing(question);
|
||||
if (lastContent is not null)
|
||||
chat.Remove(lastContent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes a question without an answer the draft again.
|
||||
/// </summary>
|
||||
/// <param name="chat">The chat.</param>
|
||||
/// <param name="question">The content of the question to edit.</param>
|
||||
/// <returns>True when the question became the draft.</returns>
|
||||
public bool TryEditLastBlock([NotNullWhen(true)] ChatThread? chat, IContent question)
|
||||
{
|
||||
if (!this.AreAllowed(chat) || question is not ContentText || !Holds(chat, question))
|
||||
return false;
|
||||
|
||||
chat.RemoveForEditing(question);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool Holds(ChatThread chat, IContent content) => chat.Blocks.Any(block => block.Content == content);
|
||||
}
|
||||
@@ -55,31 +55,31 @@
|
||||
@if (this.IsSecondToLastBlock && this.Role is ChatRole.USER && this.EditLastUserBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastUserBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" Disabled="@(!this.BlockActionsEnabled())" OnClick="@this.EditLastUserBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.USER && this.EditLastBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Edit")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" OnClick="@this.EditLastBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Default" Disabled="@(!this.BlockActionsEnabled())" OnClick="@this.EditLastBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Regenerate")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Default" Disabled="@(!this.BlockActionsEnabled() || !this.RegenerateEnabled())" OnClick="@this.RegenerateBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (!this.IsLastContentBlock && this.Role is ChatRole.AI && this.RollbackFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Roll back to this response")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Restore" Color="Color.Default" Disabled="@(!this.RollbackEnabled())" OnClick="@this.RollbackBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Restore" Color="Color.Default" Disabled="@(!this.BlockActionsEnabled())" OnClick="@this.RollbackBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.RemoveBlockFunc is not null)
|
||||
{
|
||||
<MudTooltip Text="@T("Removes this block")" Placement="Placement.Bottom">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Disabled="@(!this.BlockActionsEnabled())" OnClick="@this.RemoveBlock"/>
|
||||
</MudTooltip>
|
||||
}
|
||||
|
||||
|
||||
@@ -89,8 +89,15 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
[Parameter]
|
||||
public Func<bool> RegenerateEnabled { get; set; } = () => false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user may change the messages of the chat right now, i.e., remove, edit, regenerate, or roll back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The chat answers false while an answer is being written for it, since every change to a message
|
||||
/// would collide with the answer. Whoever passes none of these actions needs no answer here.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public Func<bool> RollbackEnabled { get; set; } = () => false;
|
||||
public Func<bool> BlockActionsEnabled { get; set; } = () => false;
|
||||
|
||||
/// <summary>
|
||||
/// What the export offers, used both as the label of the export button and as the title of
|
||||
@@ -321,6 +328,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
hash.Add(this.HideContent);
|
||||
hash.Add(this.SettingsManager.IsDarkMode);
|
||||
hash.Add(this.RegenerateEnabled());
|
||||
hash.Add(this.BlockActionsEnabled());
|
||||
hash.Add(this.Content.InitialRemoteWait);
|
||||
hash.Add(this.Content.IsStreaming);
|
||||
hash.Add(this.Content.FileAttachments.Count);
|
||||
@@ -733,7 +741,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private async Task RemoveBlock()
|
||||
{
|
||||
if (this.RemoveBlockFunc is null)
|
||||
if (this.RemoveBlockFunc is null || !this.BlockActionsEnabled())
|
||||
return;
|
||||
|
||||
var remove = await this.DialogService.ShowMessageBox(
|
||||
@@ -793,7 +801,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private async Task RegenerateBlock()
|
||||
{
|
||||
if (this.RegenerateFunc is null)
|
||||
if (this.RegenerateFunc is null || !this.BlockActionsEnabled())
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.AI)
|
||||
@@ -811,7 +819,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private async Task RollbackBlock()
|
||||
{
|
||||
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.RollbackEnabled())
|
||||
if (this.RollbackFunc is null || this.Role is not ChatRole.AI || !this.BlockActionsEnabled())
|
||||
return;
|
||||
|
||||
var rollback = await this.DialogService.ShowMessageBox(
|
||||
@@ -826,7 +834,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private async Task EditLastBlock()
|
||||
{
|
||||
if (this.EditLastBlockFunc is null)
|
||||
if (this.EditLastBlockFunc is null || !this.BlockActionsEnabled())
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.USER)
|
||||
@@ -837,7 +845,7 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
|
||||
private async Task EditLastUserBlock()
|
||||
{
|
||||
if (this.EditLastUserBlockFunc is null)
|
||||
if (this.EditLastUserBlockFunc is null || !this.BlockActionsEnabled())
|
||||
return;
|
||||
|
||||
if(this.Role is not ChatRole.USER)
|
||||
|
||||
Reference in new issue
Block a user