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

@@ -19,6 +19,7 @@ internal sealed class StructuredLlmStageRunner(
/// <typeparam name="T">The strict response type.</typeparam>
/// <param name="provider">The selected provider configuration.</param>
/// <param name="profile">The selected user profile.</param>
/// <param name="restrictions">What the content of the briefing demands, see VisualBriefingManifest.Restrictions.</param>
/// <param name="systemContract">The stage-specific system contract.</param>
/// <param name="prompt">The user prompt containing stage inputs.</param>
/// <param name="attachments">The first-turn attachments.</param>
@@ -31,6 +32,7 @@ internal sealed class StructuredLlmStageRunner(
public async Task<StructuredLlmStageResult<T>> RunAsync<T>(
ProviderSettings provider,
Profile profile,
ChatRestrictions restrictions,
string systemContract,
string prompt,
IReadOnlyList<FileAttachment> attachments,
@@ -79,6 +81,10 @@ internal sealed class StructuredLlmStageRunner(
],
};
// The thread of the stage holds what the briefing holds. The orchestrator checked the provider before the
// build started; the provider call checks it against the thread once more:
thread.RequireRestrictions(restrictions);
VisualBriefingContractIssue? repairIssue = null;
for (var attempt = 1; attempt <= 2; attempt++)
{
@@ -1,5 +1,6 @@
using System.Text.Json;
using AIStudio.Chat;
using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Tools.Media;
@@ -58,7 +59,13 @@ public partial class VisualBriefingAssistant
/// <summary>
/// Defines <c>CreateBriefingAsync</c> for the visual briefing feature.
/// </summary>
private async Task CreateBriefingAsync()
private Task CreateBriefingAsync() => this.CreateBriefingAsync(ChatRestrictions.NONE);
/// <summary>
/// Creates a briefing which holds content with the given restrictions, see VisualBriefingManifest.Restrictions.
/// </summary>
/// <param name="restrictions">What the content of the briefing demands.</param>
private async Task CreateBriefingAsync(ChatRestrictions restrictions)
{
var defaults = this.SettingsManager.ConfigurationData.VisualBriefing;
var defaultProvider = this.SettingsManager.GetPreselectedProvider(ComponentKind.VISUAL_BRIEFING_ASSISTANT);
@@ -79,7 +86,7 @@ public partial class VisualBriefingAssistant
OptimizeImages = defaults.OptimizeImages,
};
var briefing = await this.Store.CreateAsync(suggestedName, string.Empty, settings);
var briefing = await this.Store.CreateAsync(suggestedName, string.Empty, settings, restrictions);
await this.ReloadListAsync(briefing.BriefingId);
}
@@ -1,3 +1,4 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.Rust;
@@ -109,10 +110,21 @@ public partial class VisualBriefingAssistant
private string? ValidateProjectName(string name) => string.IsNullOrWhiteSpace(name) ? T("Please provide a briefing name.") : null;
/// <summary>Validates the selected generation provider.</summary>
private string? ValidateProvider(ProviderSettings value) =>
value == ProviderSettings.NONE || value.UsedLLMProvider is LLMProviders.NONE
? T("Please select a provider.")
: null;
/// <remarks>
/// A provider which may not see a text sent to this briefing is reported here, before the build starts. A provider the
/// organization prohibited is not: it fails for every briefing alike, and the build reports it.
/// </remarks>
private string? ValidateProvider(ProviderSettings value)
{
if (value == ProviderSettings.NONE || value.UsedLLMProvider is LLMProviders.NONE)
return T("Please select a provider.");
var restrictions = this.selectedBriefing?.Restrictions ?? ChatRestrictions.NONE;
if (!restrictions.IsLLMProviderAllowed(value) && ChatRestrictions.NONE.IsLLMProviderAllowed(value))
return T("This provider may not see the text sent to this briefing, because of where the text came from. Please select another provider.");
return null;
}
/// <summary>Validates the free-form target language when Other is selected.</summary>
private string? ValidateCustomTargetLanguage(string language) =>
@@ -1,6 +1,8 @@
using AIStudio.Chat;
using AIStudio.Components;
using AIStudio.Dialogs;
using AIStudio.Dialogs.Settings;
using AIStudio.Tools.AssistantLaunch;
using AIStudio.Tools.AssistantSessions;
using AIStudio.Tools.Services;
@@ -152,20 +154,18 @@ public partial class VisualBriefingAssistant : MSGComponentBase
return;
}
this.ApplyFilters([], [Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Event.CONFIGURATION_CHANGED]);
this.ApplyFilters([], [Event.CONFIGURATION_CHANGED]);
this.MediaTranscriptionService.StateChanged += this.MediaStateChanged;
this.BuildProgressService.Changed += this.BuildProgressChanged;
await this.ReloadListAsync();
await this.ConsumePendingMediaOutcomesAsync();
this.MonitorSourceStatusAsync(this.sourceMonitorCancellation.Token).Observe($"{nameof(VisualBriefingAssistant)}: monitoring the source status");
var deferredInstruction = this.MessageBus.TakeDeferredMessages<string>(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT).LastOrDefault();
if (!string.IsNullOrWhiteSpace(deferredInstruction))
if (AssistantLaunchService.TakeTextPayload(ComponentKind.VISUAL_BRIEFING_ASSISTANT) is { } textLaunchPayload && !string.IsNullOrWhiteSpace(textLaunchPayload.Text))
{
if (this.selectedBriefing is null)
await this.CreateBriefingAsync();
this.editor.Instruction = deferredInstruction;
// A text sent here starts a briefing of its own, which holds what the text demands. The briefing
// open last belongs to other work, and its instruction would be gone:
await this.CreateBriefingAsync(textLaunchPayload.Restrictions);
this.editor.Instruction = textLaunchPayload.Text;
await this.SaveCurrentAsync();
}
@@ -235,17 +235,6 @@ public partial class VisualBriefingAssistant : MSGComponentBase
/// </summary>
protected override async Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT && data is string text)
{
if (this.selectedBriefing is null)
await this.CreateBriefingAsync();
this.editor.Instruction = text;
await this.SaveCurrentAsync();
this.StateHasChanged();
return;
}
if (triggeredEvent is Event.CONFIGURATION_CHANGED)
{
// The spellchecking setting might have changed. Since this page is not re-parameterized
@@ -1,3 +1,4 @@
using AIStudio.Chat;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Tools.Rust;
@@ -233,6 +234,28 @@ internal sealed partial class VisualBriefingBuildOrchestrator
"No provider is selected.");
}
/// <summary>
/// Ensures the selected provider may see what the briefing holds.
/// </summary>
/// <remarks>
/// A text sent to the briefing demands what it demanded where it came from, see VisualBriefingManifest.Restrictions,
/// and a provider the organization prohibited may see nothing at all. The provider call of each stage would refuse
/// such a provider as well, but only once the build runs, and the stage would fail without telling why.
/// </remarks>
/// <param name="manifest">The briefing manifest.</param>
/// <param name="provider">The provider.</param>
internal static void ValidateProviderMaySeeContent(VisualBriefingManifest manifest, ProviderSettings provider)
{
if (manifest.Restrictions.IsLLMProviderAllowed(provider))
return;
throw new VisualBriefingBuildException(
VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED,
VisualBriefingBuildStage.SOURCE_PREPARATION,
"The selected provider may not see the content of this briefing.",
$"ProviderFamily={provider.UsedLLMProvider}; RequiredConfidence={manifest.Restrictions.ProviderConfidence}; DataSecurity={manifest.Restrictions.DataSecurity}.");
}
/// <summary>
/// Ensures content-generating builds have at least one source-material file.
/// </summary>
@@ -114,6 +114,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
try
{
ValidateProvider(provider);
ValidateProviderMaySeeContent(manifest, provider);
ValidateSourceMaterial(manifest, mode);
var parentContext = await this.LoadParentContextAsync(manifest, mode, parentRevisionId, token);
VisualBriefingEvidenceArtifact? reusableEvidence = null;
@@ -37,7 +37,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingContentResponse>(provider, profile, BuildSystemContract(),
var run = await stageRunner.RunAsync<VisualBriefingContentResponse>(provider, profile, manifest.Restrictions, BuildSystemContract(),
BuildPrompt(manifest, evidence, plan), [], VisualBriefingBuildStage.CONTENT, build.OperationId, build.BuildId,
response => this.ValidateResponseAndProject(manifest, plan, evidence, response), token);
@@ -38,7 +38,7 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingEvidenceResponse>(
provider, profile, BuildSystemContract(), BuildPrompt(manifest, preparedSources), preparedSources.Attachments, VisualBriefingBuildStage.EVIDENCE,
provider, profile, manifest.Restrictions, BuildSystemContract(), BuildPrompt(manifest, preparedSources), preparedSources.Attachments, VisualBriefingBuildStage.EVIDENCE,
build.OperationId, build.BuildId, response => VisualBriefingValidation.ValidateEvidence(manifest, response), token);
stage.Attempts = run.Attempts;
@@ -18,6 +18,11 @@ public enum VisualBriefingFailureCode
/// </summary>
PROVIDER_NOT_SELECTED,
/// <summary>
/// The selected provider may not see the content of the briefing.
/// </summary>
PROVIDER_NOT_ALLOWED,
/// <summary>
/// The selected model lacks a required capability.
/// </summary>
@@ -83,6 +83,7 @@ internal static class VisualBriefingFailureExtensions
private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch
{
VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."),
VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."),
VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."),
VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."),
VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."),
@@ -1,3 +1,5 @@
using AIStudio.Chat;
namespace AIStudio.Assistants.VisualBriefing;
/// <summary>
@@ -40,6 +42,18 @@ public sealed class VisualBriefingManifest
/// </summary>
public VisualBriefingLocalSettings Settings { get; set; } = new();
/// <summary>
/// What the text sent to this briefing demands, because of where it came from.
/// </summary>
/// <remarks>
/// An answer written from mails demands here what it demanded in its chat, and every stage of every build
/// holds to it. The restrictions are set when the briefing is created for the text, and they never loosen:
/// the text may still be part of the instruction, and of the versions built from it. They live here rather
/// than in the settings, which the editor replaces with every save. A briefing nothing was sent to, or one
/// written before this property existed, demands nothing.
/// </remarks>
public ChatRestrictions Restrictions { get; init; } = ChatRestrictions.NONE;
/// <summary>
/// Defines <c>Sources</c> for the visual briefing feature.
/// </summary>
@@ -42,7 +42,7 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingPlanResponse>(provider, profile, BuildSystemContract(), BuildPrompt(manifest, evidence),
var run = await stageRunner.RunAsync<VisualBriefingPlanResponse>(provider, profile, manifest.Restrictions, BuildSystemContract(), BuildPrompt(manifest, evidence),
[], VisualBriefingBuildStage.PLAN, build.OperationId, build.BuildId, response => VisualBriefingValidation.ValidatePlan(evidence, response), token);
stage.Attempts = run.Attempts;
@@ -42,7 +42,7 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingDesignResponse>(provider, profile, BuildSystemContract(),
var run = await stageRunner.RunAsync<VisualBriefingDesignResponse>(provider, profile, manifest.Restrictions, BuildSystemContract(),
BuildPrompt(manifest, plan, parentPresentation), [], VisualBriefingBuildStage.DESIGN, build.OperationId, build.BuildId,
response => ValidateDesign(manifest, plan, content, response), token);
@@ -1,5 +1,7 @@
using System.Text.Json;
using AIStudio.Chat;
namespace AIStudio.Assistants.VisualBriefing;
public sealed partial class VisualBriefingStore
@@ -294,10 +296,14 @@ public sealed partial class VisualBriefingStore
/// <summary>
/// Defines <c>CreateAsync</c> for the visual briefing feature.
/// </summary>
/// <remarks>
/// The restrictions are set here only, see VisualBriefingManifest.Restrictions: nothing later on loosens them.
/// </remarks>
public async Task<VisualBriefingManifest> CreateAsync(
string name,
string author,
VisualBriefingLocalSettings settings,
ChatRestrictions restrictions,
Guid? briefingId = null,
CancellationToken token = default)
{
@@ -330,6 +336,7 @@ public sealed partial class VisualBriefingStore
CreatedAtUtc = now,
ModifiedAtUtc = now,
Settings = settings,
Restrictions = restrictions,
};
await this.StoreManifestAtomicAsync(manifest, token);
@@ -455,6 +462,8 @@ public sealed partial class VisualBriefingStore
/// </summary>
private async Task StoreManifestAtomicAsync(VisualBriefingManifest manifest, CancellationToken token)
{
// What is written now holds everything of the current version, the restrictions among them, which an older AI Studio cannot read:
manifest.ManifestVersion = VisualBriefingVersions.MANIFEST;
var json = JsonSerializer.Serialize(manifest, JSON_OPTIONS);
await WriteTextAtomicAsync(this.ManifestPath(manifest.BriefingId), json, overwrite: true, token);
}
@@ -1,6 +1,8 @@
using System.Text;
using System.Text.Json;
using AIStudio.Chat;
namespace AIStudio.Assistants.VisualBriefing;
public sealed partial class VisualBriefingStore
@@ -339,6 +341,10 @@ public sealed partial class VisualBriefingStore
export.Name,
export.Author,
SettingsFromExport(export),
// An export is a file outside AI Studio, which carries neither the instruction nor any restrictions.
// A briefing which exists here already keeps its own:
ChatRestrictions.NONE,
export.BriefingId,
token);
}
@@ -440,6 +446,9 @@ public sealed partial class VisualBriefingStore
parts.ExportManifest.Name,
parts.ExportManifest.Author,
SettingsFromExport(parts.ExportManifest),
// An export is a file outside AI Studio, which carries neither the instruction nor any restrictions:
ChatRestrictions.NONE,
copyId,
token);
@@ -9,7 +9,11 @@ public static class VisualBriefingVersions
public const int ARTIFACT = 1;
/// <summary>Gets the project manifest contract version.</summary>
public const int MANIFEST = 1;
/// <remarks>
/// Version 2 added the restrictions of a text sent to the briefing. An older version of AI Studio cannot
/// read them, so it tells the user that the briefing needs a newer version instead of ignoring them.
/// </remarks>
public const int MANIFEST = 2;
/// <summary>Gets the canonical data schema version.</summary>
public const int SCHEMA = 2;