using System.Globalization; using AIStudio.Assistants.SlideBuilder; using AIStudio.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.PowerPoint; using Microsoft.AspNetCore.Components; namespace AIStudio.Dialogs.Settings; public partial class SettingsDialogSlideBuilder : SettingsDialogBase { [Inject] private ILogger Logger { get; init; } = null!; /// /// What the preselections of layouts offer: no preference, then the layouts of the preselected template. /// /// /// Empty while no template is preselected or the template cannot be read; the dialog then says so /// instead of offering layouts. /// private List> layoutOptions = []; /// /// The preselected template, whose slides the dialog offers; null while none is preselected or it cannot be read. /// private PresentationTemplate? template; /// /// The path the template and the layout options were read from; null before the first reading. /// private string? templatePathRead; /// /// What the preselection of the maximum number of bullet points offers: no maximum, then 1 up to the most a slide may hold. /// private IEnumerable> BulletPointMaximumOptions => [new(T("No maximum"), 0), ..Enumerable.Range(1, DataSlideBuilder.MAX_BULLET_POINTS_PER_SLIDE).Select(count => new ConfigurationSelectData(count.ToString(CultureInfo.InvariantCulture), count))]; #region Overrides of SettingsDialogBase /// protected override async Task OnInitializedAsync() { this.ReadTemplate(); await base.OnInitializedAsync(); } /// protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { // The template may change in this dialog, or through a configuration plugin: if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED) this.ReadTemplate(); await base.ProcessIncomingMessage(sendingComponent, triggeredEvent, data); } #endregion /// /// Reads the layouts and the slides of the preselected template, unless they were read from it already. /// private void ReadTemplate() { var templatePath = this.SettingsManager.ConfigurationData.SlideBuilder.TemplatePath; if (string.Equals(templatePath, this.templatePathRead, StringComparison.Ordinal)) return; this.templatePathRead = templatePath; this.template = null; this.layoutOptions = []; if (string.IsNullOrWhiteSpace(templatePath) || !File.Exists(templatePath)) return; try { this.template = PresentationTemplate.ReadFile(templatePath); } catch (Exception e) { this.Logger.LogWarning(e, "The PowerPoint template '{TemplatePath}' cannot be read.", templatePath); return; } this.layoutOptions = [new(LayoutPurposeExtensions.NoPreference, string.Empty), ..this.template.FamilyNames.Select(name => new ConfigurationSelectData(name, name))]; } /// /// Preselects a template, and forgets what was preselected for the slides of the previous one. /// /// /// The choices number the slides of a template, so they mean nothing for another one. A lock of /// the organization keeps them, though: its template and its choices belong together. /// private void SetTemplatePath(string path) { var settings = this.SettingsManager.ConfigurationData.SlideBuilder; if (string.Equals(settings.TemplatePath, path, StringComparison.Ordinal)) return; settings.TemplatePath = path; if (!AreTemplateSlideUsesLocked()) settings.PreselectedTemplateSlideUses = new(); } private static bool AreTemplateSlideUsesLocked() => ManagedConfiguration.TryGet(x => x.SlideBuilder, x => x.PreselectedTemplateSlideUses, out var meta) && meta.IsLocked; /// /// What the dialog offers for a slide of the template: editing only where the slide has placeholders for content. /// private static List> UseOptionsOf(TemplateSlide slide) => Enum.GetValues() .Where(use => use is not TemplateSlideUse.EDIT || slide.IsEditable) .Select(use => new ConfigurationSelectData(use.Name(), use)) .ToList(); /// /// What is preselected for a slide of the template, as the assistant applies it. /// /// The use; a slide which is missing is left out, and one which cannot be edited is kept unchanged instead. private TemplateSlideUse PreselectedUseOf(TemplateSlide slide) => this.template is null ? TemplateSlideUse.OMIT : this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTemplateSlideUses.ToTemplateSlideUses(this.template).UseOf(slide.Number); /// /// Preselects what the presentation does with a slide of the template. /// /// /// Leaving a slide out is the default, so it is stored as no entry. The dictionary is replaced /// instead of changed, so that no other holder of it, such as the default of a configuration /// plugin, changes along. /// private void SetPreselectedUse(int number, TemplateSlideUse use) { var uses = new Dictionary(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTemplateSlideUses); if (use is TemplateSlideUse.OMIT) uses.Remove(number); else uses[number] = use; this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedTemplateSlideUses = uses; } /// /// The name of the layout preselected for a kind of slide, as the dialog shows it. /// /// The name, or empty when the LLM decides, which it also does when the template has no layout of this name. private string PreselectedLayoutOf(LayoutPurpose purpose) => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedLayouts.TryGetValue(purpose, out var name) && this.layoutOptions.Any(option => string.Equals(option.Value, name, StringComparison.Ordinal)) ? name : string.Empty; /// /// Preselects a layout for a kind of slide, or no preference for an empty name. /// /// /// The dictionary is replaced instead of changed, so that no other holder of it, such as the /// default of a configuration plugin, changes along. /// private void SetPreselectedLayout(LayoutPurpose purpose, string name) { var layouts = new Dictionary(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedLayouts); if (name.Length == 0) layouts.Remove(purpose); else layouts[purpose] = name; this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedLayouts = layouts; } }