using AIStudio.Tools.AssistantSessions;
using ComponentKind = AIStudio.Tools.Components;
using ProviderSettings = AIStudio.Settings.Provider;
namespace AIStudio.Assistants.VisualBriefing;
public partial class VisualBriefingAssistant
{
///
/// Gets the active or canceling build session for the selected briefing.
///
private AssistantSessionSnapshot? CurrentBuildSession => this.selectedBriefing is null ? null : this.AssistantSessionService.TryGetSnapshot(CreateBuildSessionKey(this.selectedBriefing.BriefingId));
///
/// Gets whether cancellation was already requested for the selected briefing build.
///
private bool IsCurrentBuildCanceling => this.CurrentBuildSession?.Status is AssistantSessionStatus.CANCELING;
///
/// Gets whether the selected revision cannot be recompiled without model calls.
///
private bool CannotRecompile => this.IsCurrentBusy || this.selectedBriefing is null || this.selectedRevisionId == Guid.Empty || !this.SelectedVersionSupportsEdits;
///
/// Defines CannotGenerate for the visual briefing feature.
///
private bool CannotGenerate(VisualBriefingEditMode mode) =>
this.IsCurrentBusy ||
this.provider == ProviderSettings.NONE ||
string.IsNullOrWhiteSpace(this.projectName) ||
this.targetLanguage is CommonLanguages.OTHER && string.IsNullOrWhiteSpace(this.customTargetLanguage) ||
this.protectionLevel is VisualBriefingProtectionLevel.OTHER && string.IsNullOrWhiteSpace(this.customProtectionLevel) ||
mode is not VisualBriefingEditMode.CHANGE_DESIGN && !this.HasSourceMaterial ||
mode is VisualBriefingEditMode.CHANGE_DESIGN or VisualBriefingEditMode.UPDATE_CONTENT &&
!this.SelectedVersionSupportsEdits ||
mode is not VisualBriefingEditMode.CHANGE_DESIGN &&
this.selectedBriefing?.Sources.Any(source =>
source.Status is VisualBriefingSourceStatus.UNREACHABLE or VisualBriefingSourceStatus.TRANSCRIPT_OUTDATED) == true;
///
/// Defines GenerateAsync for the visual briefing feature.
///
private async Task GenerateAsync(
VisualBriefingEditMode mode,
Guid? reusableBuildId = null,
Guid? parentRevisionOverride = null)
{
if (this.selectedBriefing is null || this.CannotGenerate(mode))
return;
await this.SaveCurrentAsync(reload: true);
var generationBriefing = this.selectedBriefing;
var briefingId = generationBriefing.BriefingId;
var parentRevisionId = parentRevisionOverride ??
(generationBriefing.Versions.Count == 0 ? null : this.selectedRevisionId);
var generationProvider = this.provider;
var generationProfile = this.profile;
var sessionKey = CreateBuildSessionKey(briefingId);
if (this.AssistantSessionService.TryGetSnapshot(sessionKey)?.IsActive == true)
return;
var cancellation = new CancellationTokenSource();
var session = await this.AssistantSessionService.TryBeginAsync(
sessionKey,
this.selectedBriefing.Name,
cancellation,
null,
new(StringComparer.Ordinal),
this);
var terminalStatus = AssistantSessionStatus.FAILED;
var terminalIssue = string.Empty;
this.generatingBriefings.Add(briefingId);
this.StateHasChanged();
try
{
var generation = await this.BuildOrchestrator.BuildAsync(
generationBriefing,
mode,
parentRevisionId,
generationProvider,
generationProfile,
reusableBuildId,
cancellation.Token);
this.lastBuildDiagnostics = generation.Diagnostics;
this.latestBuild = this.BuildProgressService.GetLatest(briefingId) ??
(await this.Store.ListBuildsAsync(briefingId, cancellation.Token)).FirstOrDefault();
if (!generation.Success || generation.Version is null)
{
terminalStatus = generation.FailureCode is VisualBriefingFailureCode.CANCELED ? AssistantSessionStatus.CANCELED : AssistantSessionStatus.FAILED;
this.reusableContentBuildId = generation.CanContinueAsRebuild ? generation.Diagnostics.BuildId : null;
terminalIssue = generation.Issue;
if (terminalStatus is not AssistantSessionStatus.CANCELED)
this.Snackbar.Add(generation.Issue, Severity.Error);
return;
}
this.reusableContentBuildId = null;
var generatedBriefingIsSelected = this.selectedBriefing?.BriefingId == briefingId;
if (generatedBriefingIsSelected)
{
await this.ReloadListAsync(briefingId);
await this.SelectRevisionAsync(generation.Version.RevisionId);
}
else
{
var latest = await this.Store.LoadAsync(briefingId, cancellation.Token);
if (latest is not null)
this.UpdateProject(latest);
}
this.Snackbar.Add(T("A new visual briefing version was created."), Severity.Success);
terminalStatus = AssistantSessionStatus.COMPLETED;
}
catch (OperationCanceledException)
{
terminalStatus = AssistantSessionStatus.CANCELED;
terminalIssue = T("The visual briefing generation was canceled.");
}
catch (Exception exception)
{
terminalIssue = T("The visual briefing operation failed unexpectedly. Copy the technical details for support.");
this.Logger.LogError(
"Unexpected visual briefing UI failure. BriefingId={BriefingId} Mode={Mode} ExceptionType={ExceptionType}",
briefingId,
mode,
exception.GetType().Name);
this.Snackbar.Add(terminalIssue, Severity.Error);
}
finally
{
await this.AssistantSessionService.CompleteAsync(
sessionKey,
session.SessionId,
terminalStatus,
terminalIssue,
null,
new(StringComparer.Ordinal),
this);
this.RetireFinishedSession(sessionKey);
this.generatingBriefings.Remove(briefingId);
this.StateHasChanged();
}
}
///
/// Recompiles the selected immutable revision with the current AI Studio export pipeline.
///
/// An optional parent used while resuming a persisted operation.
private async Task RecompileAsync(Guid? parentRevisionOverride = null)
{
var parentRevisionId = parentRevisionOverride ?? this.selectedRevisionId;
if (this.selectedBriefing is null ||
this.IsCurrentBusy ||
!this.VersionSupportsSemanticEdits(parentRevisionId))
return;
var recompileBriefing = this.selectedBriefing;
var briefingId = recompileBriefing.BriefingId;
var sessionKey = CreateBuildSessionKey(briefingId);
if (this.AssistantSessionService.TryGetSnapshot(sessionKey)?.IsActive == true)
return;
var cancellation = new CancellationTokenSource();
var session = await this.AssistantSessionService.TryBeginAsync(
sessionKey,
recompileBriefing.Name,
cancellation,
null,
new(StringComparer.Ordinal),
this);
var terminalStatus = AssistantSessionStatus.FAILED;
var terminalIssue = string.Empty;
this.generatingBriefings.Add(briefingId);
this.StateHasChanged();
try
{
var result = await this.BuildOrchestrator.RecompileAsync(
recompileBriefing,
parentRevisionId,
cancellation.Token);
this.lastBuildDiagnostics = result.Diagnostics;
this.latestBuild = this.BuildProgressService.GetLatest(briefingId) ?? (await this.Store.ListBuildsAsync(briefingId, cancellation.Token)).FirstOrDefault();
if (!result.Success || result.Version is null)
{
terminalStatus = result.FailureCode is VisualBriefingFailureCode.CANCELED ? AssistantSessionStatus.CANCELED : AssistantSessionStatus.FAILED;
terminalIssue = result.Issue;
if (terminalStatus is not AssistantSessionStatus.CANCELED)
this.Snackbar.Add(result.Issue, Severity.Error);
return;
}
if (this.selectedBriefing?.BriefingId == briefingId)
{
await this.ReloadListAsync(briefingId);
await this.SelectRevisionAsync(result.Version.RevisionId);
}
else
{
var latest = await this.Store.LoadAsync(briefingId, cancellation.Token);
if (latest is not null)
this.UpdateProject(latest);
}
this.Snackbar.Add(T("The briefing was recompiled with the current AI Studio version."), Severity.Success);
terminalStatus = AssistantSessionStatus.COMPLETED;
}
catch (OperationCanceledException)
{
terminalStatus = AssistantSessionStatus.CANCELED;
terminalIssue = T("The visual briefing recompilation was canceled.");
}
catch (Exception exception)
{
terminalIssue = T("The visual briefing recompilation failed unexpectedly. Copy the technical details for support.");
this.Logger.LogError(
"Unexpected visual briefing UI failure. BriefingId={BriefingId} Mode={Mode} ExceptionType={ExceptionType}",
briefingId,
VisualBriefingEditMode.RECOMPILE,
exception.GetType().Name);
this.Snackbar.Add(terminalIssue, Severity.Error);
}
finally
{
await this.AssistantSessionService.CompleteAsync(sessionKey, session.SessionId, terminalStatus, terminalIssue, null, new(StringComparer.Ordinal), this);
this.RetireFinishedSession(sessionKey);
this.generatingBriefings.Remove(briefingId);
this.StateHasChanged();
}
}
///
/// Consumes the finished session of one briefing while this component is still showing it.
///
///
/// A briefing session carries no state, because the briefing itself is stored on disk. Its only
/// remaining purpose after completion is the indicator on the assistant overview. When the user
/// is still on this page, that indicator would be stale, so we retire the session the same way
/// AssistantBase does. When the user has navigated away, we keep it so the overview can
/// report that a background build has finished.
///
/// The session key of the briefing that just finished.
private void RetireFinishedSession(AssistantSessionKey sessionKey)
{
if (!this.isDisposed)
_ = this.AssistantSessionService.TryTakeInactiveSnapshot(sessionKey);
}
///
/// Automatically resumes the selected build that was active when the app stopped.
///
private async Task ResumeSelectedBuildAsync()
{
if (this.selectedBriefing is null)
return;
var activeBuild = (await this.Store.ListBuildsAsync(this.selectedBriefing.BriefingId))
.FirstOrDefault(build => build.Status is VisualBriefingBuildStatus.ACTIVE);
if (activeBuild is null)
return;
if (activeBuild.Mode is VisualBriefingEditMode.RECOMPILE)
{
await this.RecompileAsync(activeBuild.ParentRevisionId);
return;
}
if (this.provider == ProviderSettings.NONE)
return;
await this.GenerateAsync(
activeBuild.Mode,
reusableBuildId: null,
parentRevisionOverride: activeBuild.ParentRevisionId);
}
///
/// Applies a content-free live progress update for the selected project.
///
private void BuildProgressChanged(Guid briefingId)
{
if (this.selectedBriefing?.BriefingId != briefingId)
return;
_ = this.InvokeAsync(() =>
{
if (this.selectedBriefing?.BriefingId != briefingId)
return;
this.latestBuild = this.BuildProgressService.GetLatest(briefingId);
this.StateHasChanged();
});
}
///
/// Resumes the latest failed build with its persisted operation inputs.
///
private async Task ResumeLatestBuildAsync()
{
if (this.latestBuild?.Status is not (VisualBriefingBuildStatus.FAILED or VisualBriefingBuildStatus.CANCELED))
return;
if (this.latestBuild.Mode is VisualBriefingEditMode.RECOMPILE)
await this.RecompileAsync(this.latestBuild.ParentRevisionId);
else
await this.GenerateAsync(
this.latestBuild.Mode,
parentRevisionOverride: this.latestBuild.ParentRevisionId);
}
///
/// Requests cancellation for the build running on the selected briefing.
///
private async Task CancelCurrentBuildAsync()
{
if (this.selectedBriefing is null)
return;
var sessionKey = CreateBuildSessionKey(this.selectedBriefing.BriefingId);
if (this.AssistantSessionService.TryGetSnapshot(sessionKey)?.Status is not AssistantSessionStatus.RUNNING)
return;
await this.AssistantSessionService.CancelAsync(sessionKey, this);
this.StateHasChanged();
}
///
/// Defines CopyTechnicalDetailsAsync for the visual briefing feature.
///
private async Task CopyTechnicalDetailsAsync()
{
if (this.lastBuildDiagnostics is null)
return;
await this.RustService.CopyText2Clipboard(
this.Snackbar,
this.lastBuildDiagnostics.ToClipboardText());
}
///
/// Defines IsGenerating for the visual briefing feature.
///
private bool IsGenerating(Guid briefingId)
{
if (this.generatingBriefings.Contains(briefingId))
return true;
return this.AssistantSessionService.TryGetSnapshot(CreateBuildSessionKey(briefingId))?.IsActive == true;
}
///
/// Creates the assistant-session key used by a visual briefing build.
///
private static AssistantSessionKey CreateBuildSessionKey(Guid briefingId) => new(ComponentKind.VISUAL_BRIEFING_ASSISTANT, briefingId.ToString("D"));
}