mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 20:07:20 +00:00
Added a Send to menu to every chat message and kept restrictions when sending content on (#1124)
This commit is contained in:
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)">
|
||||
|
||||
Reference in new issue
Block a user