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")); }