Added a Send to menu to every chat message and kept restrictions when sending content on (#1124)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-10-28 13:28:30 +01:00
1 parent d06688ca08
commit bf54b8cd0d
73 files changed
+1923 -390

No files matched your search

@@ -8,6 +8,7 @@
@if (this.ChatThread is not null)
{
var blocks = this.ChatThread.Blocks.OrderBy(n => n.Time).ToList();
var sendToDestinations = this.AssistantLaunchService.GetDestinationsFor(Components.CHAT);
for (var i = 0; i < blocks.Count; i++)
{
var block = blocks[i];
@@ -30,7 +31,9 @@
RollbackFunc="@this.RollbackBlock"
BlockActionsEnabled="@(() => this.blockActions.AreAllowed(this.ChatThread))"
EditLastBlockFunc="@this.EditLastBlock"
EditLastUserBlockFunc="@this.EditLastUserBlock"/>
EditLastUserBlockFunc="@this.EditLastUserBlock"
SendToFunc="@this.SendBlockToAsync"
SendToDestinations="@sendToDestinations"/>
}
}
}
@@ -9,6 +9,7 @@ using AIStudio.Tools.Databases;
using AIStudio.Tools.Databases.ProvenanceStore;
using AIStudio.Tools.ToolCallingSystem;
using AIStudio.Tools.AIJobs;
using AIStudio.Tools.AssistantLaunch;
using AIStudio.Tools.Media;
using AIStudio.Tools.Services;
@@ -89,6 +90,9 @@ public partial class ChatComponent : MSGComponentBase
[Inject]
private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!;
[Inject]
private AssistantLaunchService AssistantLaunchService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
@@ -358,9 +362,7 @@ public partial class ChatComponent : MSGComponentBase
await this.ApplyChatTemplateToolSelectionAsync();
this.lastAppliedAutomaticDataSourceOptions = this.GetAutomaticDataSourceOptions();
var deferredInput = MessageBus.INSTANCE.TakeDeferredMessages<string>(Event.SEND_TO_CHAT_INPUT).LastOrDefault();
if (!string.IsNullOrWhiteSpace(deferredInput))
this.ComposerState.SetUserInput(deferredInput);
var deferredInput = AssistantLaunchService.TakeTextPayload(Tools.Components.CHAT);
//
// Check for deferred messages of the kind 'SEND_TO_CHAT',
@@ -455,7 +457,13 @@ public partial class ChatComponent : MSGComponentBase
//
this.ApplyAutomaticDataSourceOptions();
}
//
// A text sent here, e.g., by an assistant, goes into the field for the next message:
//
if (deferredInput is not null)
await this.PutTextIntoComposerAsync(deferredInput);
//
// Check if the user wants to show the latest message after loading:
//
@@ -1313,6 +1321,36 @@ public partial class ChatComponent : MSGComponentBase
this.draftAutosave?.Schedule(this.ChatThread);
}
/// <summary>
/// Puts a text sent here into the field for the next message, together with what it demands.
/// </summary>
/// <remarks>
/// The text demands here what it demanded where it was written: a provider or a tool which that
/// place kept from the text must not get it here either, already before the user sends it. A chat
/// without a thread therefore gets one right away, in the workspace its first message would get, see
/// ResolveWorkspaceOfFirstThreadAsync. The text counts as a message the user started, like one typed.
/// </remarks>
/// <param name="payload">The text sent, with its restrictions.</param>
private async Task PutTextIntoComposerAsync(TextLaunchPayload payload)
{
if (string.IsNullOrWhiteSpace(payload.Text))
return;
var chat = this.ChatThread;
if (chat is null)
{
chat = this.NewChatThread(string.Empty, await this.ResolveWorkspaceOfFirstThreadAsync());
this.ChatThread = chat;
this.unstoredChat = chat;
await this.PublishNewChatThreadAsync();
}
chat.RequireRestrictions(payload.Restrictions);
this.ComposerState.SetUserInput(payload.Text);
this.KeepComposerInDraft();
this.hasUnsavedChanges = true;
}
/// <summary>
/// Puts the text and the attachments of the composer into the draft of the given chat.
/// </summary>
@@ -1824,7 +1862,8 @@ public partial class ChatComponent : MSGComponentBase
/// <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>
/// <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)
/// <returns>True when the new chat started. False when the user kept the open chat, or when it could not be deleted.</returns>
private async Task<bool> 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
@@ -1849,7 +1888,7 @@ public partial class ChatComponent : MSGComponentBase
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Start New Chat"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
if (dialogResult is null || dialogResult.Canceled)
return;
return false;
}
//
@@ -1888,7 +1927,7 @@ public partial class ChatComponent : MSGComponentBase
}
if (!chatIsGone)
return;
return false;
}
//
@@ -1987,6 +2026,7 @@ public partial class ChatComponent : MSGComponentBase
await this.ApplyChatTemplateToolSelectionAsync();
this.ApplyAutomaticDataSourceOptions();
await this.SyncForegroundChatAsync();
return true;
}
private async Task MoveChatToWorkspace()
@@ -2203,6 +2243,41 @@ public partial class ChatComponent : MSGComponentBase
await MessageBus.INSTANCE.SendMessage<bool>(this, Event.WORKSPACE_TOGGLE_OVERLAY);
}
/// <summary>
/// Sends the text of a message of this chat to an assistant, or into a new chat.
/// </summary>
/// <remarks>
/// The text takes along what this chat demands, and what the AI wrote goes through the prompt injection
/// filter, see AssistantLaunchService.CreateTextPayloadAsync. Sending changes nothing in this chat.<br/><br/>
/// A new chat starts in the workspace of this chat, or as a temporary chat when this one is temporary, and
/// gets the text into its field for the next message, where the user decides what to send. It starts first:
/// when the user keeps this chat, nothing is filtered in vain. The page of the chat stays open meanwhile, so
/// the text cannot wait for it as it waits for an assistant.
/// </remarks>
/// <param name="content">The content of the message.</param>
/// <param name="destination">The assistant, or the chat for a new chat.</param>
private async Task SendBlockToAsync(IContent content, Tools.Components destination)
{
if (this.ChatThread is not { } sourceChat)
return;
if (destination is not Tools.Components.CHAT)
{
if (await this.AssistantLaunchService.CreateTextPayloadAsync(sourceChat, content) is { } payload)
await this.AssistantLaunchService.LaunchAsync(destination, payload);
return;
}
if (!await this.StartNewChat(targetWorkspaceId: sourceChat.WorkspaceId))
return;
if (await this.AssistantLaunchService.CreateTextPayloadAsync(sourceChat, content) is { } textForNewChat)
await this.PutTextIntoComposerAsync(textForNewChat);
this.StateHasChanged();
}
private async Task RemoveBlock(IContent block)
{
if (!this.blockActions.TryRemove(this.ChatThread, block))
@@ -2,7 +2,7 @@
@inherits MSGComponentBase
<MudTooltip Placement="Placement.Top" Text="@T("Start a new chat with a chat template")">
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="@Origin.BottomLeft" Class="@this.MarginClass">
<MudMenu AnchorOrigin="Origin.TopLeft" TransformOrigin="@Origin.BottomLeft" Class="@this.MarginClass" MaxHeight="@MenuDefaults.MAX_HEIGHT">
<ActivatorContent>
@if (this.CurrentChatTemplate != ChatTemplate.NO_CHAT_TEMPLATE)
{
@@ -0,0 +1,18 @@
namespace AIStudio.Components;
/// <summary>
/// Settings every menu of AI Studio shares.
/// </summary>
public static class MenuDefaults
{
/// <summary>
/// The height in pixels above which a menu scrolls.
/// </summary>
/// <remarks>
/// MudBlazor keeps a MudSelect inside the window by itself, but never a MudMenu: a menu runs past the
/// edge of the window, and its lower entries cannot be reached. A menu whose entries come from data, such
/// as the assistants or the profiles, therefore sets MaxHeight to this value. It is the height MudBlazor
/// gives a MudSelect, so both look alike.
/// </remarks>
public const int MAX_HEIGHT = 300;
}
@@ -1,7 +1,7 @@
@using AIStudio.Settings
@inherits MSGComponentBase
<MudTooltip Text="@this.ToolTipText" Placement="Placement.Top">
<MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass" Disabled="@this.Disabled">
<MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass" Disabled="@this.Disabled" MaxHeight="@MenuDefaults.MAX_HEIGHT">
<ActivatorContent>
@if (this.CurrentProfile != Profile.NO_PROFILE)
{
@@ -13,7 +13,7 @@
@this.SelectedValueName()
</MudButton>
}
<MudMenu Icon="@Icons.Material.Filled.ArrowDropDown" Style="align-self: auto;">
<MudMenu Icon="@Icons.Material.Filled.ArrowDropDown" Style="align-self: auto;" MaxHeight="@MenuDefaults.MAX_HEIGHT">
@foreach(var item in this.Items)
{
<MudMenuItem OnClick="() => this.SelectItem(item)">