using AIStudio.Dialogs; using AIStudio.Tools.Rust; using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.VisualBriefing; public partial class VisualBriefingAssistant { /// /// Gets whether the selected revision references all four intermediate artifacts. /// private bool SelectedVersionSupportsEdits => this.VersionSupportsSemanticEdits(this.selectedRevisionId); /// /// Gets whether one revision references the complete semantic artifact set. /// /// The revision to inspect. /// Whether the revision can be edited or recompiled without rebuilding its inputs. private bool VersionSupportsSemanticEdits(Guid revisionId) => this.selectedBriefing?.Versions.FirstOrDefault(version => version.RevisionId == revisionId) is { SchemaVersion: VisualBriefingVersions.SCHEMA, IntermediateArtifactVersion: VisualBriefingVersions.INTERMEDIATE_ARTIFACT, EvidenceContractVersion: VisualBriefingVersions.EVIDENCE_CONTRACT, PlanContractVersion: VisualBriefingVersions.PLAN_CONTRACT, ContentContractVersion: VisualBriefingVersions.CONTENT_CONTRACT, DesignContractVersion: VisualBriefingVersions.DESIGN_CONTRACT, EvidenceArtifactId: not null, PlanArtifactId: not null, ContentArtifactId: not null, PresentationArtifactId: not null, }; /// /// Defines CanGoBackward for the visual briefing feature. /// private bool CanGoBackward => this.GetSelectedVersionIndex() > 0; /// /// Gets whether a newer immutable revision can be selected. /// private bool CanGoForward { get { var index = this.GetSelectedVersionIndex(); return index >= 0 && index < (this.selectedBriefing?.Versions.Count ?? 0) - 1; } } /// /// Defines PreviewContainerClass for the visual briefing feature. /// private string PreviewContainerClass => $"visual-briefing-preview visual-briefing-preview-{this.previewDevice.ToString().ToLowerInvariant()}"; /// /// Defines SelectRevisionAsync for the visual briefing feature. /// private Task SelectRevisionAsync(Guid revisionId) { if (this.selectedBriefing is null || this.selectedBriefing.Versions.All(version => version.RevisionId != revisionId)) return Task.CompletedTask; this.selectedRevisionId = revisionId; var token = this.PreviewTokenService.Issue(this.selectedBriefing.BriefingId, revisionId); this.previewUrl = $"/visual-briefing/preview/{this.selectedBriefing.BriefingId:D}/{revisionId:D}?token={Uri.EscapeDataString(token)}"; return Task.CompletedTask; } /// /// Defines PreviousVersionAsync for the visual briefing feature. /// private async Task PreviousVersionAsync() { var versions = this.OrderedVersions(); var index = this.GetSelectedVersionIndex(); if (index > 0) await this.SelectRevisionAsync(versions[index - 1].RevisionId); } /// /// Defines NextVersionAsync for the visual briefing feature. /// private async Task NextVersionAsync() { var versions = this.OrderedVersions(); var index = this.GetSelectedVersionIndex(); if (index >= 0 && index < versions.Count - 1) await this.SelectRevisionAsync(versions[index + 1].RevisionId); } /// /// Defines ExportAsync for the visual briefing feature. /// private async Task ExportAsync() { if (this.selectedBriefing is null || this.selectedRevisionId == Guid.Empty) return; var sourcePath = await this.Store.GetVersionPathAsync(this.selectedBriefing.BriefingId, this.selectedRevisionId); if (sourcePath is null) return; if (!await this.ConfirmLargeFileAsync(sourcePath, T("export"))) return; var response = await this.RustService.SaveFile( T("Export visual briefing"), [FileTypes.VISUAL_BRIEFING_HTML], $"{SafeFileName(this.editor.Name)}.html"); if (response.UserCancelled) return; if (PathComparer().Equals(Path.GetFullPath(sourcePath), Path.GetFullPath(response.SaveFilePath))) { await this.MessageBus.SendError(new(Icons.Material.Filled.SaveAs, T("Choose a different export location so the immutable briefing version is not overwritten."))); return; } var verified = await this.Store.OpenIntegrityCheckedVersionAsync(this.selectedBriefing.BriefingId, this.selectedRevisionId); if (verified is null) { await this.MessageBus.SendError(new(Icons.Material.Filled.GppBad, T("The selected briefing version failed its integrity check and cannot be exported."))); return; } await using var source = verified.Value.Stream; await using var destination = new FileStream(response.SaveFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 65_536, true); await source.CopyToAsync(destination); var exportedVersion = this.selectedBriefing.Versions.First(version => version.RevisionId == this.selectedRevisionId); this.Logger.LogInformation( new EventId((int)VisualBriefingLogEventId.EXPORT, VisualBriefingLogEventId.EXPORT.ToString()), "Visual briefing version exported. OperationId={OperationId} BuildId={BuildId} BriefingId={BriefingId} RevisionId={RevisionId} DocumentHash={DocumentHash} Bytes={Bytes}", exportedVersion.OperationId, exportedVersion.BuildId, this.selectedBriefing.BriefingId, exportedVersion.RevisionId, exportedVersion.DocumentHash, source.Length); await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileDownload, T("The visual briefing was exported."))); } /// /// Defines ImportAsync for the visual briefing feature. /// private async Task ImportAsync() { var response = await this.RustService.SelectFile(T("Import visual briefing"), [FileTypes.VISUAL_BRIEFING_HTML]); if (response.UserCancelled || !await this.ConfirmLargeFileAsync(response.SelectedFilePath, T("import"))) return; var imported = await this.Store.ImportAsync(response.SelectedFilePath, importNameConflictAsCopy: false); if (imported.RequiresCopyConfirmation) { var parameters = new DialogParameters { { dialog => dialog.Message, T("This briefing ID already exists under another name. Import it as a copy with a new ID?") }, }; var reference = await this.DialogService.ShowAsync(T("Import as copy"), parameters, DialogOptions.FULLSCREEN); var result = await reference.Result; if (result is null || result.Canceled) return; imported = await this.Store.ImportAsync(response.SelectedFilePath, importNameConflictAsCopy: true); } if (!imported.Success) { await this.MessageBus.SendError(new(Icons.Material.Filled.FileUpload, imported.Issue)); return; } await this.ReloadListAsync(imported.BriefingId); await this.SelectRevisionAsync(imported.RevisionId); this.Logger.LogInformation( new EventId((int)VisualBriefingLogEventId.IMPORT, VisualBriefingLogEventId.IMPORT.ToString()), "Visual briefing version imported. BriefingId={BriefingId} RevisionId={RevisionId} Deduplicated={Deduplicated}", imported.BriefingId, imported.RevisionId, imported.WasDeduplicated); await this.MessageBus.SendSuccess(new(Icons.Material.Filled.FileUpload, imported.WasDeduplicated ? T("This briefing revision was already imported.") : T("The visual briefing was imported."))); } /// /// Defines OrderedVersions for the visual briefing feature. /// private IReadOnlyList OrderedVersions() => this.selectedBriefing?.Versions.OrderBy(version => version.VersionNumber).ToArray() ?? []; /// /// Defines GetSelectedVersionIndex for the visual briefing feature. /// private int GetSelectedVersionIndex() { var versions = this.OrderedVersions(); for (var index = 0; index < versions.Count; index++) if (versions[index].RevisionId == this.selectedRevisionId) return index; return -1; } /// /// Defines SafeFileName for the visual briefing feature. /// private static string SafeFileName(string value) { var invalid = Path.GetInvalidFileNameChars().ToHashSet(); var name = new string(value.Select(character => invalid.Contains(character) ? '-' : character).ToArray()).Trim(); return string.IsNullOrWhiteSpace(name) ? "visual-briefing" : name; } }