mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-05 04:27:21 +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
@@ -0,0 +1,60 @@
|
||||
using System.Collections.Immutable;
|
||||
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings.DataModel;
|
||||
using AIStudio.Tools.Web;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
/// <summary>
|
||||
/// What a chat demands of everybody who continues with its content, because of what it read.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The things a thread ratchets, see ChatThread.RequiredProviderConfidence, ChatThread.DataSecurity,
|
||||
/// ChatThread.RequiredOutboundDataRestriction, and ChatThread.WebAddressesWrittenByModels, as one value which can
|
||||
/// leave the thread. Content which goes from one thread to another carries what its source demands: a chat which
|
||||
/// read mails demands the same of an assistant its answer is sent to. Each part only ever tightens, and NONE
|
||||
/// demands nothing.
|
||||
/// </remarks>
|
||||
/// <param name="ProviderConfidence">The minimum confidence of a provider which may see the content.</param>
|
||||
/// <param name="DataSecurity">Whether only self-hosted providers may see the content; NOT_SPECIFIED demands nothing.</param>
|
||||
/// <param name="OutboundData">Where the content may still be sent, and which source demands it.</param>
|
||||
/// <param name="WebAddressesWrittenByModels">The web addresses a model wrote into the content, as request keys, see WebAddresses.CreateRequestKey. They never count as given by the user, wherever the content goes.</param>
|
||||
public sealed record ChatRestrictions(ConfidenceLevel ProviderConfidence, DataSourceSecurity DataSecurity, OutboundDataRequirement OutboundData, ImmutableHashSet<string> WebAddressesWrittenByModels)
|
||||
{
|
||||
/// <summary>
|
||||
/// Demands nothing: what a chat holds before it read anything which restricts it.
|
||||
/// </summary>
|
||||
public static readonly ChatRestrictions NONE = new(ConfidenceLevel.NONE, DataSourceSecurity.NOT_SPECIFIED, OutboundDataRequirement.NONE, []);
|
||||
|
||||
/// <summary>
|
||||
/// The restrictions which demand everything this one and another one demand.
|
||||
/// </summary>
|
||||
/// <param name="other">The other restrictions.</param>
|
||||
/// <returns>The stricter one of each part, and the addresses written by models of both.</returns>
|
||||
public ChatRestrictions StricterOf(ChatRestrictions other) => new(
|
||||
other.ProviderConfidence > this.ProviderConfidence ? other.ProviderConfidence : this.ProviderConfidence,
|
||||
this.DataSecurity.TightenedBy(other.DataSecurity),
|
||||
this.OutboundData.StricterOf(other.OutboundData),
|
||||
this.WebAddressesWrittenByModels.Union(other.WebAddressesWrittenByModels));
|
||||
|
||||
/// <summary>
|
||||
/// These restrictions, with every web address in a text a model wrote.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For a text which leaves the place where a model wrote it, such as an answer sent to an assistant. There, the
|
||||
/// text becomes a message of the user, and its addresses would pass for ones the user gave, see
|
||||
/// ChatThread.IsWebAddressGivenToTheModel. A model could have put anything of the chat it came from into them.
|
||||
/// </remarks>
|
||||
/// <param name="text">The text a model wrote.</param>
|
||||
/// <returns>The restrictions, with the request keys of the addresses in the text added.</returns>
|
||||
public ChatRestrictions WithWebAddressesWrittenByModelIn(string text)
|
||||
{
|
||||
var requestKeys = this.WebAddressesWrittenByModels.ToBuilder();
|
||||
foreach (var address in WebAddresses.Find(text))
|
||||
if (WebAddresses.TryCreateRequestKey(address, out var requestKey))
|
||||
requestKeys.Add(requestKey);
|
||||
|
||||
return this with { WebAddressesWrittenByModels = requestKeys.ToImmutable() };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
public static class ChatRestrictionsExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the specified provider may see content with these restrictions.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A provider the organization prohibited is never allowed, whatever the restrictions.<br/><br/>
|
||||
///
|
||||
/// We don't check if the provider is allowed to use the data sources the content came from.
|
||||
/// That kind of check is done when the available data sources are resolved. What counts here is
|
||||
/// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed.
|
||||
/// </remarks>
|
||||
/// <param name="restrictions">What the content demands.</param>
|
||||
/// <param name="provider">The provider to check.</param>
|
||||
/// <returns>True, when the provider may see the content. False, otherwise.</returns>
|
||||
public static bool IsLLMProviderAllowed<T>(this ChatRestrictions restrictions, T provider)
|
||||
{
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
|
||||
// A provider the organization prohibited is allowed nowhere, not even for content which demands nothing:
|
||||
if (settingsManager.IsProviderProhibited(GetLLMProvider(provider)))
|
||||
return false;
|
||||
|
||||
var providerConfidence = provider switch
|
||||
{
|
||||
IProvider p => p.GetConfidenceLevel(settingsManager),
|
||||
AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager),
|
||||
|
||||
_ => ConfidenceLevel.UNKNOWN,
|
||||
};
|
||||
|
||||
//
|
||||
// The confidence axis is checked on its own: a provider trusted by configuration counts as
|
||||
// self-hosted for data-source security, which is the check further down, but that trust
|
||||
// says nothing about how confidential the provider is. An organization which wants its
|
||||
// contractually covered cloud provider to pass here raises its level through the custom
|
||||
// confidence scheme instead.
|
||||
//
|
||||
if (providerConfidence < restrictions.ProviderConfidence)
|
||||
return false;
|
||||
|
||||
// The data security is not specified. Means, the content never came from RAG, or RAG was
|
||||
// enabled, but no data sources were selected. That's fine:
|
||||
if (restrictions.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
|
||||
return true;
|
||||
|
||||
//
|
||||
// Is the provider trusted for data-source security checks?
|
||||
//
|
||||
var isTrustedProvider = provider switch
|
||||
{
|
||||
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
//
|
||||
// Check the data security of the content against the selected provider:
|
||||
//
|
||||
return isTrustedProvider switch
|
||||
{
|
||||
// The provider is trusted -- it may see data of any data source:
|
||||
true => true,
|
||||
|
||||
// The provider is not trusted -- it depends on the data security of the content:
|
||||
false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
|
||||
private static LLMProviders GetLLMProvider<T>(T provider) => provider switch
|
||||
{
|
||||
IProvider p => p.Provider,
|
||||
AIStudio.Settings.Provider p => p.UsedLLMProvider,
|
||||
|
||||
_ => LLMProviders.NONE,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
@@ -144,15 +145,11 @@ public sealed record ChatThread
|
||||
/// any provider marks the chat as one which holds data of a data source, while a restriction set
|
||||
/// earlier stays. NOT_SPECIFIED demands nothing and changes nothing.<br/><br/>
|
||||
/// Shared by the RAG process and by tools which search the data sources, so both tighten a chat
|
||||
/// the same way.
|
||||
/// the same way. The rule itself is DataSourceSecurityExtensions.TightenedBy, which
|
||||
/// ChatRestrictions uses as well for content which leaves the chat.
|
||||
/// </remarks>
|
||||
/// <param name="dataSecurity">What the data brought in demands.</param>
|
||||
public void RequireDataSecurity(DataSourceSecurity dataSecurity) => this.DataSecurity = (this.DataSecurity, dataSecurity) switch
|
||||
{
|
||||
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED,
|
||||
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY,
|
||||
_ => this.DataSecurity,
|
||||
};
|
||||
public void RequireDataSecurity(DataSourceSecurity dataSecurity) => this.DataSecurity = this.DataSecurity.TightenedBy(dataSecurity);
|
||||
|
||||
/// <summary>
|
||||
/// Where this chat may still send data, because of the internal content it holds: mails, a
|
||||
@@ -184,16 +181,56 @@ public sealed record ChatThread
|
||||
/// For an assistant which lets tools work in a thread of its own and carries the result over into
|
||||
/// the thread it shows: without this, the result would arrive without the restrictions its data
|
||||
/// demands, and a chat started from it could hand that data to any provider or tool. All three
|
||||
/// ratchets go along, and each one only tightens.
|
||||
/// ratchets go along, and so do the web addresses its models wrote; each part only tightens.
|
||||
/// </remarks>
|
||||
/// <param name="otherThread">The thread whose result this chat takes over.</param>
|
||||
public void RequireRestrictionsOf(ChatThread otherThread)
|
||||
public void RequireRestrictionsOf(ChatThread otherThread) => this.RequireRestrictions(otherThread.CaptureRestrictions());
|
||||
|
||||
/// <summary>
|
||||
/// What this chat demands of everybody who continues with its content, as it stands right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A snapshot for content which leaves this chat, such as a message sent to an assistant: the chat
|
||||
/// may tighten later on, while an answer is still being written, but the content which left does
|
||||
/// not hold what arrives afterward. A method rather than a property, so that the snapshot is not
|
||||
/// stored along with the chat.
|
||||
/// </remarks>
|
||||
/// <returns>The restrictions of this chat.</returns>
|
||||
public ChatRestrictions CaptureRestrictions() => new(this.RequiredProviderConfidence, this.DataSecurity, this.RequiredOutboundDataRestriction, this.WebAddressesWrittenByModels);
|
||||
|
||||
/// <summary>
|
||||
/// Tightens this chat to everything the given restrictions demand, and never loosens it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For content which arrives with the restrictions of where it was written, see CaptureRestrictions.
|
||||
/// Each part tightens on its own, by the same rules as when this chat reads something itself.
|
||||
/// </remarks>
|
||||
/// <param name="restrictions">What the content arriving demands.</param>
|
||||
public void RequireRestrictions(ChatRestrictions restrictions)
|
||||
{
|
||||
this.RequireProviderConfidence(otherThread.RequiredProviderConfidence);
|
||||
this.RequireDataSecurity(otherThread.DataSecurity);
|
||||
this.RequireOutboundDataRestriction(otherThread.RequiredOutboundDataRestriction);
|
||||
this.RequireProviderConfidence(restrictions.ProviderConfidence);
|
||||
this.RequireDataSecurity(restrictions.DataSecurity);
|
||||
this.RequireOutboundDataRestriction(restrictions.OutboundData);
|
||||
this.WebAddressesWrittenByModels = this.WebAddressesWrittenByModels.Union(restrictions.WebAddressesWrittenByModels);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The web addresses a model wrote which reached this chat inside a message of the user, as request
|
||||
/// keys, see WebAddresses.CreateRequestKey.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// An answer of a model which is sent on, to an assistant or into a new chat, becomes a message of the
|
||||
/// user there. Its addresses would then pass for ones the user gave, see IsWebAddressGivenToTheModel,
|
||||
/// although a model chose them and could have put anything of the chat it came from into them. This
|
||||
/// set keeps them apart, see ChatRestrictions.WithWebAddressesWrittenByModelIn.<br/><br/>
|
||||
///
|
||||
/// Stored with the chat, unlike the other sets of addresses: forgetting it after a restart would let
|
||||
/// the chat know more addresses, not fewer. It only ever grows, like the other restrictions, and it is
|
||||
/// immutable, so that a copy of the thread never shares a set which grows later.
|
||||
/// </remarks>
|
||||
[JsonInclude]
|
||||
public ImmutableHashSet<string> WebAddressesWrittenByModels { get; private set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// The name of the chat thread. Usually generated by an AI model or manually edited by the user.
|
||||
/// </summary>
|
||||
@@ -259,8 +296,10 @@ public sealed record ChatThread
|
||||
/// message of the user or a document attached to one, see ContentText.RuntimeAttachmentWebAddresses,
|
||||
/// or a tool returned it, see RuntimeWebAddressesFromTools. What the model wrote itself never
|
||||
/// counts, its answers included, because the model could have put anything of the chat into it.
|
||||
/// The comparison is the one of WebAddresses.CreateRequestKey, so an address counts when it
|
||||
/// asks the server for the same.<br/><br/>
|
||||
/// Neither does what any model wrote and what reached this chat as a message of the user, such
|
||||
/// as an answer sent here from another chat, see WebAddressesWrittenByModels. The comparison is
|
||||
/// the one of WebAddresses.CreateRequestKey, so an address counts when it asks the server for
|
||||
/// the same.<br/><br/>
|
||||
/// Shared by both rules of Read Web Page which depend on it: a free address choice switched
|
||||
/// off, and a mailbox which allows the addresses from the chat only.
|
||||
/// </remarks>
|
||||
@@ -281,6 +320,10 @@ public sealed record ChatThread
|
||||
if (messagesOfTheUser.Any(content => content.RuntimeAttachmentWebAddresses.Contains(requestKey)))
|
||||
return true;
|
||||
|
||||
// A model wrote it, even though it stands in a message of the user:
|
||||
if (this.WebAddressesWrittenByModels.Contains(requestKey))
|
||||
return false;
|
||||
|
||||
return messagesOfTheUser
|
||||
.SelectMany(content => WebAddresses.Find(content.Text))
|
||||
.Any(address => WebAddresses.TryCreateRequestKey(address, out var key) && string.Equals(key, requestKey, StringComparison.Ordinal));
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Chat;
|
||||
|
||||
public static class ChatThreadExtensions
|
||||
@@ -14,80 +10,16 @@ public static class ChatThreadExtensions
|
||||
///
|
||||
/// We don't check if the provider is allowed to use the data sources of the chat thread.
|
||||
/// That kind of check is done when the available data sources are resolved.<br/><br/>
|
||||
///
|
||||
///
|
||||
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
|
||||
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
|
||||
/// from the data sources is stored in the chat thread. This means we must check if the
|
||||
/// selected provider is allowed to use this thread's data security and confidence level.
|
||||
/// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left
|
||||
/// the thread is checked with as well.
|
||||
/// </remarks>
|
||||
/// <param name="chatThread">The chat thread to check.</param>
|
||||
/// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param>
|
||||
/// <param name="provider">The provider to check.</param>
|
||||
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
|
||||
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider)
|
||||
{
|
||||
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
|
||||
|
||||
// A provider the organization prohibited is allowed in no chat, not even in a new one:
|
||||
if (settingsManager.IsProviderProhibited(GetLLMProvider(provider)))
|
||||
return false;
|
||||
|
||||
// No chat thread available means we have a new chat. That's fine:
|
||||
if (chatThread is null)
|
||||
return true;
|
||||
|
||||
var providerConfidence = provider switch
|
||||
{
|
||||
IProvider p => p.GetConfidenceLevel(settingsManager),
|
||||
AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager),
|
||||
|
||||
_ => ConfidenceLevel.UNKNOWN,
|
||||
};
|
||||
|
||||
//
|
||||
// The confidence axis is checked on its own: a provider trusted by configuration counts as
|
||||
// self-hosted for data-source security, which is the check further down, but that trust
|
||||
// says nothing about how confidential the provider is. An organization which wants its
|
||||
// contractually covered cloud provider to pass here raises its level through the custom
|
||||
// confidence scheme instead.
|
||||
//
|
||||
if (providerConfidence < chatThread.RequiredProviderConfidence)
|
||||
return false;
|
||||
|
||||
// The chat thread is available, but the data security is not specified.
|
||||
// Means, we never used RAG or RAG was enabled, but no data sources were selected.
|
||||
// That's fine as well:
|
||||
if (chatThread.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
|
||||
return true;
|
||||
|
||||
//
|
||||
// Is the provider trusted for data-source security checks?
|
||||
//
|
||||
var isTrustedProvider = provider switch
|
||||
{
|
||||
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
|
||||
|
||||
_ => false,
|
||||
};
|
||||
|
||||
//
|
||||
// Check the chat data security against the selected provider:
|
||||
//
|
||||
return isTrustedProvider switch
|
||||
{
|
||||
// The provider is trusted -- we can use any data source:
|
||||
true => true,
|
||||
|
||||
// The provider is not trusted -- it depends on the data security of the chat thread:
|
||||
false => chatThread.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
|
||||
};
|
||||
}
|
||||
|
||||
private static LLMProviders GetLLMProvider<T>(T provider) => provider switch
|
||||
{
|
||||
IProvider p => p.Provider,
|
||||
AIStudio.Settings.Provider p => p.UsedLLMProvider,
|
||||
|
||||
_ => LLMProviders.NONE,
|
||||
};
|
||||
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider);
|
||||
}
|
||||
@@ -86,7 +86,7 @@
|
||||
@if (this.CanExportAnswer)
|
||||
{
|
||||
<MudTooltip Text="@this.EffectiveExportTitle" Placement="Placement.Bottom">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Save">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Save" MaxHeight="@MenuDefaults.MAX_HEIGHT">
|
||||
@foreach (var documentFormat in FileExportFormatExtensions.DOCUMENT_FORMATS)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.ExportDocument(documentFormat))" Icon="@documentFormat.ToIcon()" Label="@documentFormat.ToName()"/>
|
||||
@@ -96,7 +96,7 @@
|
||||
<MudDivider/>
|
||||
@foreach (var filesOfAKind in this.MessageFilesByKind)
|
||||
{
|
||||
<MudMenu StartIcon="@ExportGroupIcon(filesOfAKind)" Label="@this.ExportGroupLabel(filesOfAKind)">
|
||||
<MudMenu StartIcon="@ExportGroupIcon(filesOfAKind)" Label="@this.ExportGroupLabel(filesOfAKind)" MaxHeight="@MenuDefaults.MAX_HEIGHT">
|
||||
@foreach (var messageFile in filesOfAKind)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.ExportFile(messageFile))" Icon="@messageFile.ToIcon()" Label="@this.ExportLabel(messageFile)"/>
|
||||
@@ -120,6 +120,17 @@
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (this.CanSendTo)
|
||||
{
|
||||
<MudTooltip Text="@T("Send to ...")" Placement="Placement.Bottom">
|
||||
<MudMenu Icon="@Icons.Material.Filled.Apps" Disabled="@(!this.IsSendToEnabled)" MaxHeight="@MenuDefaults.MAX_HEIGHT">
|
||||
@foreach (var destination in this.SendToDestinations)
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => this.SendTo(destination))" Label="@destination.Name()"/>
|
||||
}
|
||||
</MudMenu>
|
||||
</MudTooltip>
|
||||
}
|
||||
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium" IsAIGenerated="@this.GetsAIGeneratedContentDisclosure"/>
|
||||
</div>
|
||||
</CardHeaderActions>
|
||||
|
||||
@@ -141,6 +141,25 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
[Parameter]
|
||||
public bool AddsAIGeneratedContentDisclosure { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Sends the text of this block to the given assistant, or into a new chat.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Null hides the menu for it. An assistant shows its result with this block as well, and it offers
|
||||
/// sending the result through its own footer instead.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public Func<IContent, Tools.Components, Task>? SendToFunc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The assistants and the chat the menu offers, in its order, see AssistantLaunchService.GetDestinationsFor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Whoever passes SendToFunc passes them as well, so that a chat asks for them once for all of its blocks.
|
||||
/// </remarks>
|
||||
[Parameter]
|
||||
public IReadOnlyList<Tools.Components> SendToDestinations { get; set; } = [];
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
@@ -199,6 +218,21 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
/// </remarks>
|
||||
private bool CanSaveCodeBlocks => this.Role is ChatRole.AI or ChatRole.USER && this.CanExport;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block offers its text to an assistant or a new chat.
|
||||
/// </summary>
|
||||
private bool CanSendTo => this.SendToFunc is not null && this.SendToDestinations.Count > 0 && this.Role is ChatRole.AI or ChatRole.USER && this.Content is ContentText;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the text of this block can be sent right now.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Like the export, the menu waits for the stream to finish: half an answer is nothing anybody wants
|
||||
/// to continue with. Sending changes nothing in this chat, so an answer written meanwhile for another
|
||||
/// block does not stand in the way.
|
||||
/// </remarks>
|
||||
private bool IsSendToEnabled => this.Content is ContentText { InitialRemoteWait: false, IsStreaming: false } text && !string.IsNullOrWhiteSpace(text.Text);
|
||||
|
||||
/// <summary>
|
||||
/// Whether this block leaves AI Studio with the notice that AI generated it, when that is switched on.
|
||||
/// </summary>
|
||||
@@ -420,6 +454,10 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
hash.Add(this.Content.IsStreaming);
|
||||
hash.Add(this.Content.FileAttachments.Count);
|
||||
hash.Add(this.Content.Sources.Count);
|
||||
hash.Add(this.SendToFunc is not null);
|
||||
hash.Add(this.SendToDestinations.Count);
|
||||
foreach (var destination in this.SendToDestinations)
|
||||
hash.Add(destination);
|
||||
|
||||
switch (this.Content)
|
||||
{
|
||||
@@ -836,6 +874,14 @@ public partial class ContentBlockComponent : MSGComponentBase
|
||||
if (remove.HasValue && remove.Value)
|
||||
await this.RemoveBlockFunc(this.Content);
|
||||
}
|
||||
|
||||
private async Task SendTo(Tools.Components destination)
|
||||
{
|
||||
if (this.SendToFunc is null || !this.IsSendToEnabled)
|
||||
return;
|
||||
|
||||
await this.SendToFunc(this.Content, destination);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exports the entire message, ending with the notice that AI generated it when that is switched on.
|
||||
|
||||
Reference in new issue
Block a user