Added a button to regenerate the last AI response (#247)

This commit is contained in:
Thorsten Sommer 2025-01-03 21:18:27 +01:00 committed by GitHub
parent 5e445f09fa
commit b2ca49ab92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 109 additions and 31 deletions

View File

@ -102,12 +102,40 @@ public sealed record ChatThread
/// Removes a content block from this chat thread.
/// </summary>
/// <param name="content">The content block to remove.</param>
public void Remove(IContent content)
/// <param name="removeForRegenerate">Indicates whether the content block is removed for
/// regeneration purposes. True, when the content block is removed for regeneration purposes,
/// which will not remove the previous user block if it is hidden from the user.</param>
public void Remove(IContent content, bool removeForRegenerate = false)
{
var block = this.Blocks.FirstOrDefault(x => x.Content == content);
if(block is null)
return;
//
// Remove the previous user block if it is hidden from the user. Otherwise,
// the experience might be confusing for the user.
//
// Explanation, using the ERI assistant as an example:
// - The ERI assistant generates for every file a hidden user prompt.
// - In the UI, the user can only see the AI's responses, not the hidden user prompts.
// - Now, the user removes one AI response
// - The hidden user prompt is still there, but the user can't see it.
// - Since the user prompt is hidden, neither is it possible to remove nor edit it.
// - This method solves this issue by removing the hidden user prompt when the AI response is removed.
//
if (block.Role is ChatRole.AI && !removeForRegenerate)
{
var sortedBlocks = this.Blocks.OrderBy(x => x.Time).ToList();
var index = sortedBlocks.IndexOf(block);
if (index > 0)
{
var previousBlock = sortedBlocks[index - 1];
if (previousBlock.Role is ChatRole.USER && previousBlock.HideFromUser)
this.Blocks.Remove(previousBlock);
}
}
// Remove the block from the chat thread:
this.Blocks.Remove(block);
}
}

View File

@ -12,6 +12,12 @@
<MudText Typo="Typo.body1">@this.Role.ToName() (@this.Time)</MudText>
</CardHeaderContent>
<CardHeaderActions>
@if (this.IsLastContentBlock && this.Role is ChatRole.AI && this.RegenerateFunc is not null)
{
<MudTooltip Text="Regenerate" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Recycling" Color="Color.Tertiary" OnClick="@this.RegenerateBlock"/>
</MudTooltip>
}
@if (this.RemoveBlockFunc is not null)
{
<MudTooltip Text="Removes this block" Placement="Placement.Bottom">

View File

@ -41,9 +41,18 @@ public partial class ContentBlockComponent : ComponentBase
[Parameter]
public string Class { get; set; } = string.Empty;
[Parameter]
public bool IsLastContentBlock { get; set; } = false;
[Parameter]
public Func<IContent, Task>? RemoveBlockFunc { get; set; }
[Parameter]
public Func<IContent, Task>? RegenerateFunc { get; set; }
[Parameter]
public Func<bool> RegenerateEnabled { get; set; } = () => false;
[Inject]
private RustService RustService { get; init; } = null!;
@ -143,4 +152,22 @@ public partial class ContentBlockComponent : ComponentBase
if (remove.HasValue && remove.Value)
await this.RemoveBlockFunc(this.Content);
}
private async Task RegenerateBlock()
{
if (this.RegenerateFunc is null)
return;
if(this.Role is not ChatRole.AI)
return;
var regenerate = await this.DialogService.ShowMessageBox(
"Regenerate Message",
"Do you really want to regenerate this message?",
"Yes, regenerate it",
"No, keep it");
if (regenerate.HasValue && regenerate.Value)
await this.RegenerateFunc(this.Content);
}
}

View File

@ -7,11 +7,14 @@
<ChildContent>
@if (this.ChatThread is not null)
{
@foreach (var block in this.ChatThread.Blocks.OrderBy(n => n.Time))
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
for (var i = 0; i < blocks.Count; i++)
{
var block = blocks[i];
var isLastBlock = i == blocks.Count - 1;
@if (!block.HideFromUser)
{
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content" RemoveBlockFunc="@this.RemoveBlock"/>
<ContentBlockComponent Role="@block.Role" Type="@block.ContentType" Time="@block.Time" Content="@block.Content" RemoveBlockFunc="@this.RemoveBlock" IsLastContentBlock="@isLastBlock" RegenerateFunc="@this.RegenerateBlock" RegenerateEnabled="@(() => this.IsProviderSelected)"/>
}
}
}

View File

@ -242,7 +242,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
}
}
private async Task SendMessage()
private async Task SendMessage(bool reuseLastUserPrompt = false)
{
if (!this.IsProviderSelected)
return;
@ -252,8 +252,6 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
await this.inputField.BlurAsync();
// Create a new chat thread if necessary:
var threadName = this.ExtractThreadName(this.userInput);
if (this.ChatThread is null)
{
this.ChatThread = new()
@ -263,7 +261,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
SystemPrompt = SystemPrompts.DEFAULT,
WorkspaceId = this.currentWorkspaceId,
ChatId = Guid.NewGuid(),
Name = threadName,
Name = this.ExtractThreadName(this.userInput),
Seed = this.RNG.Next(),
Blocks = [],
};
@ -274,34 +272,37 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
{
// Set the thread name if it is empty:
if (string.IsNullOrWhiteSpace(this.ChatThread.Name))
this.ChatThread.Name = threadName;
this.ChatThread.Name = this.ExtractThreadName(this.userInput);
// Update provider and profile:
this.ChatThread.SelectedProvider = this.Provider.Id;
this.ChatThread.SelectedProfile = this.currentProfile.Id;
}
//
// Add the user message to the thread:
//
var time = DateTimeOffset.Now;
this.ChatThread?.Blocks.Add(new ContentBlock
if (!reuseLastUserPrompt)
{
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
//
// Add the user message to the thread:
//
this.ChatThread?.Blocks.Add(new ContentBlock
{
Text = this.userInput,
},
});
Time = time,
ContentType = ContentType.TEXT,
Role = ChatRole.USER,
Content = new ContentText
{
Text = this.userInput,
},
});
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
this.StateHasChanged();
// Save the chat:
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
await this.SaveThread();
this.hasUnsavedChanges = false;
this.StateHasChanged();
}
}
//
@ -582,6 +583,18 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
this.StateHasChanged();
}
private async Task RegenerateBlock(IContent aiBlock)
{
if(this.ChatThread is null)
return;
this.ChatThread.Remove(aiBlock, removeForRegenerate: true);
this.hasUnsavedChanges = true;
this.StateHasChanged();
await this.SendMessage(reuseLastUserPrompt: true);
}
#region Overrides of MSGComponentBase
public override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default

View File

@ -1,2 +1,3 @@
# v0.9.24, build 199 (2025-01-xx xx:xx UTC)
- Added a button to remove a message from the chat thread.
- Added a button to regenerate the last AI response.