mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-11-04 12:27:21 +00:00
Added PowerPoint templates to the Slide Assistant (#1055)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
This commit is contained in:
1 parent
8c5cfa78b6
commit
6eb23ebbcb
136 files changed
+16535
-522
No files matched your search
@@ -0,0 +1,15 @@
|
||||
<MudExpansionPanels Dense="@true" Elevation="0" Class="@this.Class">
|
||||
<MudExpansionPanel Dense="@true" Class="border-solid border rounded pt-n4" Style="border-color: #BDBDBD" Expanded="@this.Expanded" ExpandedChanged="@this.ExpandedChanged">
|
||||
<TitleContent>
|
||||
<div class="d-flex align-center">
|
||||
<MudIcon Icon="@this.HeaderIcon" Color="@this.IconColor" Class="mr-3"/>
|
||||
<MudText Typo="Typo.button">
|
||||
@this.HeaderText
|
||||
</MudText>
|
||||
</div>
|
||||
</TitleContent>
|
||||
<ChildContent>
|
||||
@this.ChildContent
|
||||
</ChildContent>
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// A single, dense panel which hides content most users do not need, such as advanced options
|
||||
/// or the source code of a generated plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike the ExpansionPanel component, it brings its own panel list, so it stands anywhere in a
|
||||
/// form. The header text comes from the caller, which keeps the translation with the caller as
|
||||
/// well.
|
||||
/// </remarks>
|
||||
public partial class CompactExpansionPanel : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public string HeaderText { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string HeaderIcon { get; set; } = Icons.Material.Filled.Tune;
|
||||
|
||||
[Parameter]
|
||||
public Color IconColor { get; set; } = Color.Inherit;
|
||||
|
||||
/// <summary>
|
||||
/// Class names for the surrounding panel list, separated by space, e.g., for its margins.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool Expanded { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<bool> ExpandedChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
}
|
||||
@@ -1,27 +1,30 @@
|
||||
@inherits ConfigurationBaseCore
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Immediate="@true"
|
||||
Underline="false"
|
||||
Class="flex-grow-1"
|
||||
/>
|
||||
<PathDropZone Frameless="@true" Disabled="@(() => this.IsDisabled)" OnPathsDropped="@this.PathsDropped" Context="isDropTarget">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Style="@(isDropTarget ? PathDropZone.FIELD_HIGHLIGHT_STYLE : string.Empty)">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Placeholder="@(this.IsDisabled ? string.Empty : T("You can also drag & drop the folder here."))"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@(isDropTarget ? Color.Primary : this.IconColor)"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Immediate="@true"
|
||||
Underline="false"
|
||||
Class="flex-grow-1"
|
||||
/>
|
||||
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@(this.IsDisabled || this.isDirectoryDialogOpen)"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenDirectoryDialog">
|
||||
@T("Choose Directory")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@(this.IsDisabled || this.isDirectoryDialogOpen)"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenDirectoryDialog">
|
||||
@T("Choose Directory")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</PathDropZone>
|
||||
@@ -41,6 +41,9 @@ public partial class ConfigurationDirectory : ConfigurationBaseCore
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ConfigurationDirectory> Logger { get; init; } = null!;
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private bool isDirectoryDialogOpen;
|
||||
|
||||
@@ -105,6 +108,26 @@ public partial class ConfigurationDirectory : ConfigurationBaseCore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the first dropped path which leads to a folder.
|
||||
/// </summary>
|
||||
/// <param name="paths">The dropped paths.</param>
|
||||
private async Task PathsDropped(List<string> paths)
|
||||
{
|
||||
var path = DroppedFolder.FirstUsable(paths);
|
||||
if (path is null)
|
||||
{
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a directory.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, DroppedFolder.WarningFor(paths)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogInformation("The user dropped the directory '{DroppedDirectory}'.", path);
|
||||
this.timer.Stop();
|
||||
this.internalText = path;
|
||||
await this.OptionChanged(path);
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
{
|
||||
this.TextUpdate(updatedText);
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
@inherits ConfigurationBaseCore
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@this.IconColor"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Immediate="@true"
|
||||
Underline="false"
|
||||
Class="flex-grow-1"
|
||||
/>
|
||||
<PathDropZone Frameless="@true" Disabled="@(() => this.IsDisabled)" OnPathsDropped="@this.PathsDropped" Context="isDropTarget">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2" Style="@(isDropTarget ? PathDropZone.FIELD_HIGHLIGHT_STYLE : string.Empty)">
|
||||
<MudTextField
|
||||
T="string"
|
||||
Text="@this.Text()"
|
||||
TextChanged="@this.InternalUpdate"
|
||||
Disabled="@this.IsDisabled"
|
||||
Placeholder="@(this.IsDisabled ? string.Empty : T("You can also drag & drop the file here."))"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@this.Icon"
|
||||
AdornmentColor="@(isDropTarget ? Color.Primary : this.IconColor)"
|
||||
UserAttributes="@SPELLCHECK_ATTRIBUTES"
|
||||
Immediate="@true"
|
||||
Underline="false"
|
||||
Class="flex-grow-1"
|
||||
/>
|
||||
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@(this.IsDisabled || this.isFileDialogOpen)"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
<MudButton StartIcon="@Icons.Material.Filled.FolderOpen"
|
||||
Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
Size="Size.Small"
|
||||
Disabled="@(this.IsDisabled || this.isFileDialogOpen)"
|
||||
Class="mb-1"
|
||||
OnClick="@this.OpenFileDialog">
|
||||
@T("Choose File")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</PathDropZone>
|
||||
@@ -48,6 +48,9 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ConfigurationFile> Logger { get; init; } = null!;
|
||||
|
||||
private string internalText = string.Empty;
|
||||
private bool isFileDialogOpen;
|
||||
private readonly Timer timer = new(TimeSpan.FromMilliseconds(500))
|
||||
@@ -111,6 +114,26 @@ public partial class ConfigurationFile : ConfigurationBaseCore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes the first dropped path which leads to a usable file.
|
||||
/// </summary>
|
||||
/// <param name="paths">The dropped paths.</param>
|
||||
private async Task PathsDropped(List<string> paths)
|
||||
{
|
||||
var path = DroppedFile.FirstUsable(paths, this.Filter);
|
||||
if (path is null)
|
||||
{
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a file.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, DroppedFile.WarningFor(paths)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogInformation("The user dropped the file '{DroppedFilePath}'.", path);
|
||||
this.timer.Stop();
|
||||
this.internalText = path;
|
||||
await this.OptionChanged(path);
|
||||
}
|
||||
|
||||
private async Task OptionChanged(string updatedText)
|
||||
{
|
||||
this.TextUpdate(updatedText);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
@using AIStudio.Tools.PowerPoint
|
||||
@foreach (var area in Enum.GetValues<LayoutPurposeArea>())
|
||||
{
|
||||
<CompactExpansionPanel HeaderText="@this.HeaderTextOf(area)" HeaderIcon="@IconOf(area)" Class="mb-3 rounded" Expanded="@this.expandedAreas.Contains(area)" ExpandedChanged="@(expanded => this.SetExpanded(area, expanded))">
|
||||
@if (this.expandedAreas.Contains(area))
|
||||
{
|
||||
@foreach (var purpose in Enum.GetValues<LayoutPurpose>().Where(purpose => purpose.Area() == area))
|
||||
{
|
||||
@this.ChoiceContent(purpose)
|
||||
}
|
||||
}
|
||||
</CompactExpansionPanel>
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using AIStudio.Tools.PowerPoint;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Groups the choices of a layout per kind of slide into one collapsed panel per area, e.g., teaching
|
||||
/// or science, since more than a hundred choices in one list help nobody.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The choice itself comes from the caller: the Slide Assistant uses a plain select, and its
|
||||
/// settings dialog a configuration select. The header of an area tells how many of its kinds have a
|
||||
/// layout, so that no choice hides in a collapsed panel.
|
||||
///
|
||||
/// The choices of an area render only while its panel is open. A collapsed panel still keeps its
|
||||
/// content in the page, and all areas together hold more than a hundred selects, which the Slide
|
||||
/// Assistant would otherwise render again with every update while the LLM plans.
|
||||
/// </remarks>
|
||||
public partial class LayoutChoicePanels : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders the choice of a layout for one kind of slide.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment<LayoutPurpose> ChoiceContent { get; set; } = _ => _ => { };
|
||||
|
||||
/// <summary>
|
||||
/// Whether a layout is chosen for a kind of slide.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<LayoutPurpose, bool> IsChosen { get; set; } = _ => false;
|
||||
|
||||
private readonly HashSet<LayoutPurposeArea> expandedAreas = [];
|
||||
|
||||
private string HeaderTextOf(LayoutPurposeArea area)
|
||||
{
|
||||
var chosen = Enum.GetValues<LayoutPurpose>().Count(purpose => purpose.Area() == area && this.IsChosen(purpose));
|
||||
return chosen == 0 ? area.Label() : $"{area.Label()} ({chosen})";
|
||||
}
|
||||
|
||||
private void SetExpanded(LayoutPurposeArea area, bool expanded)
|
||||
{
|
||||
if (expanded)
|
||||
this.expandedAreas.Add(area);
|
||||
else
|
||||
this.expandedAreas.Remove(area);
|
||||
}
|
||||
|
||||
private static string IconOf(LayoutPurposeArea area) => area switch
|
||||
{
|
||||
LayoutPurposeArea.FRAMING => Icons.Material.Filled.FormatListNumbered,
|
||||
LayoutPurposeArea.GENERAL => Icons.Material.Filled.ViewQuilt,
|
||||
LayoutPurposeArea.TEACHING => Icons.Material.Filled.School,
|
||||
LayoutPurposeArea.BUSINESS => Icons.Material.Filled.BusinessCenter,
|
||||
LayoutPurposeArea.SCIENCE => Icons.Material.Filled.Science,
|
||||
LayoutPurposeArea.MEDICINE => Icons.Material.Filled.MedicalServices,
|
||||
LayoutPurposeArea.LAW => Icons.Material.Filled.Gavel,
|
||||
|
||||
_ => Icons.Material.Filled.ViewQuilt,
|
||||
};
|
||||
}
|
||||
@@ -141,6 +141,15 @@ public partial class PathDropZone : MSGComponentBase
|
||||
[Inject]
|
||||
private ILogger<PathDropZone> Logger { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Marks a field in a frameless zone while a path hovers over it, e.g., an option in the settings.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Such a field sits in the frame of its option already, so it gets no drop frame of its own. An
|
||||
/// outline takes no room, which keeps the layout still while the cursor moves over the field.
|
||||
/// </remarks>
|
||||
public const string FIELD_HIGHLIGHT_STYLE = "outline: 2px dashed var(--mud-palette-primary); outline-offset: 4px; border-radius: 4px;";
|
||||
|
||||
private const string FRAME_CLASSES = "relative rounded-lg border-2 border-dashed mud-width-full";
|
||||
private const string DEFAULT_SPACING_CLASSES = "pa-3 mb-3";
|
||||
|
||||
|
||||
@@ -2,9 +2,6 @@ using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
// This component has a parameter called Directory, which would shadow the file system's Directory class:
|
||||
using IODirectory = System.IO.Directory;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class SelectDirectory : MSGComponentBase
|
||||
@@ -90,35 +87,18 @@ public partial class SelectDirectory : MSGComponentBase
|
||||
/// <summary>
|
||||
/// Takes the first dropped path which leads to a folder.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A dropped file is rejected instead of being taken as its parent folder. Everything a folder
|
||||
/// contains is processed, so guessing the parent of a mistakenly dropped file could pull in far
|
||||
/// more data than the user meant to hand over.
|
||||
/// </remarks>
|
||||
/// <param name="paths">The dropped paths.</param>
|
||||
private async Task PathsDropped(List<string> paths)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
var path = DroppedFolder.FirstUsable(paths);
|
||||
if (path is null)
|
||||
{
|
||||
if (!IODirectory.Exists(path))
|
||||
continue;
|
||||
|
||||
this.Logger.LogInformation("The user dropped the directory '{DroppedDirectory}'.", path);
|
||||
this.InternalDirectoryChanged(path);
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a directory.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, DroppedFolder.WarningFor(paths)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a directory.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.GetDropWarning(paths)));
|
||||
}
|
||||
|
||||
private string GetDropWarning(List<string> paths)
|
||||
{
|
||||
// Naming the actual mistake beats a generic "that did not work". Dropping a file onto a
|
||||
// folder picker is the likeliest of them:
|
||||
if (paths.Any(File.Exists))
|
||||
return T("Please drop a folder, not a file.");
|
||||
|
||||
return T("The dropped folder could not be accessed. Please choose it with the folder chooser instead.");
|
||||
this.Logger.LogInformation("The user dropped the directory '{DroppedDirectory}'.", path);
|
||||
this.InternalDirectoryChanged(path);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,6 @@ using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
// This component has a parameter called File, which would shadow the file system's File class:
|
||||
using IOFile = System.IO.File;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class SelectFile : MSGComponentBase
|
||||
@@ -94,42 +91,18 @@ public partial class SelectFile : MSGComponentBase
|
||||
/// <summary>
|
||||
/// Takes the first dropped path which leads to a usable file.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This component carries exactly one file, so a multi-selection cannot be honored as a whole.
|
||||
/// A dropped folder is rejected instead of being read as "the first file inside it": the user
|
||||
/// was asked for a file, and picking one for them would be a surprise.
|
||||
/// </remarks>
|
||||
/// <param name="paths">The dropped paths.</param>
|
||||
private async Task PathsDropped(List<string> paths)
|
||||
{
|
||||
foreach (var path in paths)
|
||||
var path = DroppedFile.FirstUsable(paths, this.Filter);
|
||||
if (path is null)
|
||||
{
|
||||
if (!IOFile.Exists(path))
|
||||
continue;
|
||||
|
||||
if (this.Filter is { Length: > 0 } && !FileTypes.IsAllowedPath(path, this.Filter))
|
||||
continue;
|
||||
|
||||
this.Logger.LogInformation("The user dropped the file '{DroppedFilePath}'.", path);
|
||||
this.InternalFileChanged(path);
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a file.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, DroppedFile.WarningFor(paths)));
|
||||
return;
|
||||
}
|
||||
|
||||
this.Logger.LogWarning("None of the {Count} dropped path(s) could be used as a file.", paths.Count);
|
||||
await this.MessageBus.SendWarning(new(Icons.Material.Filled.Warning, this.GetDropWarning(paths)));
|
||||
}
|
||||
|
||||
private string GetDropWarning(List<string> paths)
|
||||
{
|
||||
// Naming the actual mistake beats a generic "that did not work". Dropping a folder onto a
|
||||
// file picker is the likeliest of them:
|
||||
if (paths.Any(Directory.Exists))
|
||||
return T("Please drop a file, not a folder.");
|
||||
|
||||
// The file exists, so the file type filter is what turned it down:
|
||||
if (paths.Any(IOFile.Exists))
|
||||
return T("Please drop a file with a supported file type.");
|
||||
|
||||
return T("The dropped file could not be accessed. Please choose it with the file chooser instead.");
|
||||
this.Logger.LogInformation("The user dropped the file '{DroppedFilePath}'.", path);
|
||||
this.InternalFileChanged(path);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user