diff --git a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Build.cs b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Build.cs index 1603bf67..d00f3701 100644 --- a/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Build.cs +++ b/app/MindWork AI Studio/Assistants/VisualBriefing/VisualBriefingAssistant.razor.Build.cs @@ -39,171 +39,59 @@ public partial class VisualBriefingAssistant source.Status is VisualBriefingSourceStatus.UNREACHABLE or VisualBriefingSourceStatus.TRANSCRIPT_OUTDATED) == true; /// - /// Defines GenerateAsync for the visual briefing feature. + /// Runs one long-running briefing operation inside the shared session, progress, and error envelope. /// - private async Task GenerateAsync( - VisualBriefingEditMode mode, - Guid? reusableBuildId = null, - Guid? parentRevisionOverride = null) + /// + /// Generating a new version and recompiling an existing one differ only in the guard, the call they + /// make, and the messages they show. Everything around that is identical: the per-briefing session, + /// the busy marker, the diagnostics, the reload of either the editor or the background list entry, + /// and the terminal status. Keeping that envelope in one place is what makes both paths behave the + /// same when an operation is canceled or fails unexpectedly. + /// + /// The briefing the operation runs on. + /// The edit mode, used for diagnostics. + /// The orchestrator call to run. + /// The message shown after a new version was committed. + /// The issue recorded when the user canceled the operation. + /// The issue recorded when the operation threw. + /// A task that completes once the operation reached a terminal state. + private async Task RunBriefingOperationAsync(VisualBriefingManifest briefing, VisualBriefingEditMode mode, Func> operation, + string successMessage, string canceledMessage, string unexpectedFailureMessage) { - 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.editor.Provider; - var generationProfile = this.editor.Profile; - + var briefingId = briefing.BriefingId; var sessionKey = CreateBuildSessionKey(briefingId); if (this.AssistantSessionService.TryGetSnapshot(sessionKey)?.IsActive == true) return; + // The session service disposes this token source when the session completes: var cancellation = new CancellationTokenSource(); - var session = await this.AssistantSessionService.TryBeginAsync( - sessionKey, - this.selectedBriefing.Name, - cancellation, - null, - new(StringComparer.Ordinal), - this); + var session = await this.AssistantSessionService.TryBeginAsync(sessionKey, briefing.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); - + var result = await operation(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; + this.reusableContentBuildId = result.CanContinueAsRebuild ? result.Diagnostics.BuildId : null; + terminalIssue = result.Issue; if (terminalStatus is not AssistantSessionStatus.CANCELED) this.Snackbar.Add(result.Issue, Severity.Error); - + return; } + this.reusableContentBuildId = null; if (this.selectedBriefing?.BriefingId == briefingId) { await this.ReloadListAsync(briefingId); @@ -216,22 +104,18 @@ public partial class VisualBriefingAssistant this.UpdateProject(latest); } - this.Snackbar.Add(T("The briefing was recompiled with the current AI Studio version."), Severity.Success); + this.Snackbar.Add(successMessage, Severity.Success); terminalStatus = AssistantSessionStatus.COMPLETED; } catch (OperationCanceledException) { terminalStatus = AssistantSessionStatus.CANCELED; - terminalIssue = T("The visual briefing recompilation was canceled."); + terminalIssue = canceledMessage; } 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); + terminalIssue = unexpectedFailureMessage; + 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 @@ -243,6 +127,52 @@ public partial class VisualBriefingAssistant } } + /// + /// Generates a new immutable version of the selected briefing. + /// + /// The edit mode to run. + /// An optional build whose validated content is reused. + /// An optional parent used while resuming a persisted operation. + private async Task GenerateAsync(VisualBriefingEditMode mode, Guid? reusableBuildId = null, Guid? parentRevisionOverride = null) + { + if (this.selectedBriefing is null || this.CannotGenerate(mode)) + return; + + // Saving reloads the list, which replaces the selected manifest. Everything below must use the + // reloaded instance, so the briefing is captured only after the save: + await this.SaveCurrentAsync(reload: true); + var generationBriefing = this.selectedBriefing; + var parentRevisionId = parentRevisionOverride ?? (generationBriefing.Versions.Count == 0 ? null : this.selectedRevisionId); + var generationProvider = this.editor.Provider; + var generationProfile = this.editor.Profile; + + await this.RunBriefingOperationAsync(generationBriefing, mode, token => this.BuildOrchestrator.BuildAsync(generationBriefing, mode, + parentRevisionId, generationProvider, generationProfile, reusableBuildId, token), + T("A new visual briefing version was created."), + T("The visual briefing generation was canceled."), + T("The visual briefing operation failed unexpectedly. Copy the technical details for support.")); + } + + /// + /// 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; + await this.RunBriefingOperationAsync( + recompileBriefing, + VisualBriefingEditMode.RECOMPILE, + token => this.BuildOrchestrator.RecompileAsync(recompileBriefing, parentRevisionId, token), + T("The briefing was recompiled with the current AI Studio version."), + T("The visual briefing recompilation was canceled."), + T("The visual briefing recompilation failed unexpectedly. Copy the technical details for support.")); + } + /// /// Consumes the finished session of one briefing while this component is still showing it. ///