mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-07-16 23:02:56 +00:00
parent
1d31fd9a22
commit
5b685193ef
15
app/MindWork AI Studio/Components/CodeBlock.razor
Normal file
15
app/MindWork AI Studio/Components/CodeBlock.razor
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
@if (!this.IsInline)
|
||||
{
|
||||
@if (this.ParentTabs is null)
|
||||
{
|
||||
<MudPaper Class="code-block no-elevation" Style="@this.BlockPadding()">
|
||||
<pre><code>@this.ChildContent</code></pre>
|
||||
</MudPaper>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="inline-code-block"><kbd>@this.ChildContent</kbd></span>
|
||||
}
|
||||
|
40
app/MindWork AI Studio/Components/CodeBlock.razor.cs
Normal file
40
app/MindWork AI Studio/Components/CodeBlock.razor.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class CodeBlock : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Title { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public bool IsInline { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public CodeTabs? ParentTabs { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (this.ParentTabs is not null && this.Title is not null)
|
||||
{
|
||||
void BlockSelf(RenderTreeBuilder builder)
|
||||
{
|
||||
builder.OpenComponent<CodeBlock>(0);
|
||||
builder.AddAttribute(1, "Title", this.Title);
|
||||
builder.AddAttribute(2, "ChildContent", this.ChildContent);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
this.ParentTabs.RegisterBlock(this.Title, BlockSelf);
|
||||
}
|
||||
}
|
||||
|
||||
private string BlockPadding()
|
||||
{
|
||||
return this.ParentTabs is null ? "padding: 16px !important;" : "padding: 8px !important";
|
||||
}
|
||||
}
|
11
app/MindWork AI Studio/Components/CodeTabs.razor
Normal file
11
app/MindWork AI Studio/Components/CodeTabs.razor
Normal file
@ -0,0 +1,11 @@
|
||||
<MudTabs @bind-ActivePanelIndex="selectedIndex" PanelClass="code-block" MinimumTabWidth="30px" Class="mt-2">
|
||||
@foreach (var block in blocks)
|
||||
{
|
||||
<MudTabPanel Text="@block.Title">
|
||||
@block.Fragment
|
||||
</MudTabPanel>
|
||||
}
|
||||
</MudTabs>
|
||||
<CascadingValue Value="this">
|
||||
@this.ChildContent
|
||||
</CascadingValue>
|
30
app/MindWork AI Studio/Components/CodeTabs.razor.cs
Normal file
30
app/MindWork AI Studio/Components/CodeTabs.razor.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
public partial class CodeTabs : ComponentBase
|
||||
{
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
private readonly List<CodeTabItem> blocks = new();
|
||||
private int selectedIndex;
|
||||
|
||||
internal void RegisterBlock(string title, RenderFragment fragment)
|
||||
{
|
||||
this.blocks.Add(new CodeTabItem
|
||||
{
|
||||
Title = title,
|
||||
Fragment = fragment,
|
||||
});
|
||||
|
||||
this.StateHasChanged();
|
||||
}
|
||||
|
||||
private class CodeTabItem
|
||||
{
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public RenderFragment Fragment { get; init; } = null!;
|
||||
}
|
||||
}
|
154
app/MindWork AI Studio/Dialogs/PandocDialog.razor
Normal file
154
app/MindWork AI Studio/Dialogs/PandocDialog.razor
Normal file
@ -0,0 +1,154 @@
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
Install Pandoc
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (this.showInstallPage)
|
||||
{
|
||||
<div class="mb-4">
|
||||
<MudText Class="mb-2">
|
||||
AI Studio relies on the <strong>free and open-sourced</strong> third-party app <strong>Pandoc</strong> to process and retrieve data from local
|
||||
Office files (ex. <strong>Word</strong>) and later other text formats like LaTeX.
|
||||
</MudText>
|
||||
<MudText>
|
||||
Unfortunately Pandoc's GPL license is not compatible with AI Studios licences, nonetheless software under GPL is generally free to use and
|
||||
free of charge as well.
|
||||
Therefore you have to accept Pandoc's GPL license before we can download and install Pandoc for free
|
||||
automatically for you <strong>(recommended)</strong>.
|
||||
However you can download it yourself manually with the instructions below.
|
||||
</MudText>
|
||||
<MudExpansionPanels>
|
||||
<MudExpansionPanel Text="GNU General Public License v2 (GPL)" MaxHeight="300" ExpandedChanged="OnExpandedChanged">
|
||||
@if (this.isLoading)
|
||||
{
|
||||
<MudSkeleton />
|
||||
<MudSkeleton Animation="Animation.Wave" />
|
||||
<MudSkeleton />
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(this.licenseText))
|
||||
{
|
||||
<MudJustifiedText>@this.licenseText</MudJustifiedText>
|
||||
}
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
</div>
|
||||
<MudExpansionPanels Class="mb-3" MultiExpansion="@false" Outlined="false" Elevation="0">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.AutoFixHigh" HeaderText="Automatic installation" IsExpanded="true">
|
||||
<MudText Typo="Typo.caption">
|
||||
Pandoc is distributed under the
|
||||
<MudLink Typo="Typo.caption" Href="https://github.com/jgm/pandoc/blob/main/COPYRIGHT" Target="_blank">GNU General Public License v2 (GPL)</MudLink>.
|
||||
By clicking "Accept GPL and Install", you agree to the terms of the GPL license <br/> and Pandoc
|
||||
will be installed automatically for you. Software under GPL is <strong>free of charge</strong> and free to use.<br/>
|
||||
</MudText>
|
||||
<MudButton OnClick="InstallPandocAsync" Color="Color.Primary" Class="mt-4" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.InstallDesktop">
|
||||
Accept GPL and install for free
|
||||
</MudButton>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Build" HeaderText="Manual installation">
|
||||
<MudText Class="mb-2">
|
||||
If you prefer to install Pandoc yourself, please follow one of these two guides. Installers are only available for Windows and Mac.
|
||||
</MudText>
|
||||
<MudExpansionPanels Outlined="false" Elevation="0">
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.AppRegistration" HeaderText="Download with installer" IsExpanded="true">
|
||||
<MudList T="string">
|
||||
<MudListItem T="string" Class="mb-2">
|
||||
Accept the terms of the GPL license and download the latest installer with the download button below.
|
||||
Eventually you need to allow the download of the installer in the download window.
|
||||
<CodeTabs>
|
||||
<CodeBlock Title="Windows">pandoc-@(PANDOC_VERSION)-windows-x86_64.msi</CodeBlock>
|
||||
<CodeBlock Title="Mac OS x86">pandoc-@(PANDOC_VERSION)-x86_64-macOS.pkg</CodeBlock>
|
||||
<CodeBlock Title="Mac OS ARM">pandoc-@(PANDOC_VERSION)-arm64-macOS.pkg</CodeBlock>
|
||||
</CodeTabs>
|
||||
</MudListItem>
|
||||
<MudListItem T="string">
|
||||
Execute the installer and follow the instructions.
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
<MudText Class="mb-3" Typo="Typo.caption">
|
||||
Pandoc is distributed under the <MudLink Typo="Typo.caption" Href="https://github.com/jgm/pandoc/blob/main/COPYRIGHT" Target="_blank">GNU General Public License v2 (GPL)</MudLink>.
|
||||
By clicking "Accept GPL and download installer", you agree to the terms of the GPL license. Software under GPL is <strong>free of charge</strong> and free to use.<br/>
|
||||
</MudText>
|
||||
<MudButton OnClick="@this.GetInstaller" Color="Color.Secondary" Class="mt-4" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Downloading">
|
||||
Accept GPL and download installer
|
||||
</MudButton>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Outlined.Archive" HeaderText="Download with archive">
|
||||
<MudList T="string">
|
||||
<MudListItem T="string" Class="mb-2">
|
||||
Accept the terms of the GPL license and download the latest archive with the download button below.
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Class="mb-2">
|
||||
Extract the archive to a folder of your choice.
|
||||
<CodeTabs>
|
||||
<CodeBlock Title="Windows">C:\Users\%USERNAME%\pandoc</CodeBlock>
|
||||
<CodeBlock Title="Mac OS">/usr/local/bin/pandoc</CodeBlock>
|
||||
<CodeBlock Title="Linux">/usr/local/bin/pandoc</CodeBlock>
|
||||
</CodeTabs>
|
||||
</MudListItem>
|
||||
<MudListItem T="string" Class="mb-2">
|
||||
Open the folder and copy the full path to the <CodeBlock IsInline="@true">pandoc.exe</CodeBlock> file into your
|
||||
clipboard.
|
||||
<CodeTabs>
|
||||
<CodeBlock Title="Windows">C:\Users\%USERNAME%\pandoc\pandoc-@(PANDOC_VERSION)</CodeBlock>
|
||||
<CodeBlock Title="Mac OS">/usr/local/bin/pandoc/pandoc-@(PANDOC_VERSION)</CodeBlock>
|
||||
<CodeBlock Title="Linux">/usr/local/bin/pandoc/pandoc-@(PANDOC_VERSION)</CodeBlock>
|
||||
</CodeTabs>
|
||||
</MudListItem>
|
||||
<MudListItem T="string">
|
||||
Add the copied path to your systems environment variables and check the installation
|
||||
by typing <br/><CodeBlock IsInline="@true">pandoc --version</CodeBlock>
|
||||
into your command line interface.
|
||||
<CodeTabs>
|
||||
<CodeBlock Title="Windows">> pandoc.exe --version<br/>> pandoc.exe @(PANDOC_VERSION)</CodeBlock>
|
||||
<CodeBlock Title="Mac OS">> pandoc --version<br/>> pandoc.exe @(PANDOC_VERSION)</CodeBlock>
|
||||
<CodeBlock Title="Linux">> pandoc --version<br/>> pandoc.exe @(PANDOC_VERSION)</CodeBlock>
|
||||
</CodeTabs>
|
||||
</MudListItem>
|
||||
</MudList>
|
||||
<MudText Class="mb-3" Typo="Typo.caption">
|
||||
Pandoc is distributed under the <MudLink Typo="Typo.caption" Href="https://github.com/jgm/pandoc/blob/main/COPYRIGHT" Target="_blank">GNU General Public License v2 (GPL)</MudLink>.
|
||||
By clicking "Accept GPL and archive", you agree to the terms of the GPL license. Software under GPL is <strong>free of charge</strong> and free to use.<br/>
|
||||
</MudText>
|
||||
<MudButton OnClick="@this.GetArchive" Color="Color.Secondary" Class="mt-4" Variant="Variant.Filled" Size="Size.Small" StartIcon="@Icons.Material.Filled.Downloading">
|
||||
Accept GPL and download archive
|
||||
</MudButton>
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
</ExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
<div class="mt-2">
|
||||
<MudButton OnClick="@this.RejectLicense" Variant="Variant.Text" Color="Color.Default">Reject GPL licence</MudButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudItem Class="px-8 py-2" Style="height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||
@if (showSkeleton)
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Circle" Animation="Animation.Pulse" Class="mb-4"
|
||||
Style="width: 4em; height: 4em;"/>
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Animation="Animation.Pulse" Width="230px"
|
||||
Height="35px"/>
|
||||
}
|
||||
else if (isPandocAvailable)
|
||||
{
|
||||
<MudIcon Class="mb-2" Style="width: 2.5em; height: 2.5em;" Icon="@Icons.Material.Filled.Check"
|
||||
Color="Color.Success"/>
|
||||
<MudText Typo="Typo.subtitle1" Align="Align.Center">
|
||||
Pandoc ist auf Ihrem System verfügbar
|
||||
</MudText>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Class="mb-2" Style="width: 3.5em; height: 3.5em;" Icon="@Icons.Material.Filled.Error" Color="Color.Error"/>
|
||||
<MudText Class="mb-6" Typo="Typo.subtitle1" Align="Align.Center">
|
||||
Pandoc ist auf Ihrem System nicht verfügbar
|
||||
</MudText>
|
||||
<MudButton Color="Color.Primary" OnClick="@this.ProceedToInstallation" Variant="Variant.Filled">
|
||||
Proceed to installation
|
||||
</MudButton>
|
||||
}
|
||||
</MudItem>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
136
app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs
Normal file
136
app/MindWork AI Studio/Dialogs/PandocDialog.razor.cs
Normal file
@ -0,0 +1,136 @@
|
||||
using AIStudio.Tools.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Dialogs;
|
||||
|
||||
public partial class PandocDialog : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
private HttpClient HttpClient { get; set; } = null!;
|
||||
|
||||
[Inject]
|
||||
private RustService RustService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[CascadingParameter]
|
||||
private IMudDialogInstance MudDialog { get; set; } = null!;
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PandocDialog");
|
||||
private static readonly string LICENCE_URI = "https://raw.githubusercontent.com/jgm/pandoc/master/COPYRIGHT";
|
||||
private static string PANDOC_VERSION = "1.0.0";
|
||||
|
||||
private bool isPandocAvailable;
|
||||
private bool showSkeleton;
|
||||
private bool showInstallPage;
|
||||
private string? licenseText;
|
||||
private bool isLoading;
|
||||
|
||||
#region Overrides of ComponentBase
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
this.showSkeleton = true;
|
||||
await this.CheckPandocAvailabilityAsync();
|
||||
PANDOC_VERSION = await Pandoc.FetchLatestVersionAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private void Cancel() => this.MudDialog.Cancel();
|
||||
|
||||
private async Task CheckPandocAvailabilityAsync()
|
||||
{
|
||||
this.isPandocAvailable = await Pandoc.CheckAvailabilityAsync(this.RustService);
|
||||
this.showSkeleton = false;
|
||||
await this.InvokeAsync(this.StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task InstallPandocAsync()
|
||||
{
|
||||
await Pandoc.InstallAsync(this.RustService);
|
||||
this.MudDialog.Close(DialogResult.Ok(true));
|
||||
await this.DialogService.ShowAsync<PandocDialog>("pandoc dialog");
|
||||
}
|
||||
|
||||
private void ProceedToInstallation() => this.showInstallPage = true;
|
||||
|
||||
private async Task GetInstaller()
|
||||
{
|
||||
var uri = await Pandoc.GenerateInstallerUriAsync();
|
||||
var filename = this.FilenameFromUri(uri);
|
||||
await this.JsRuntime.InvokeVoidAsync("triggerDownload", uri, filename);
|
||||
}
|
||||
|
||||
private async Task GetArchive()
|
||||
{
|
||||
var uri = await Pandoc.GenerateUriAsync();
|
||||
var filename = this.FilenameFromUri(uri);
|
||||
await this.JsRuntime.InvokeVoidAsync("triggerDownload", uri, filename);
|
||||
}
|
||||
|
||||
private async Task RejectLicense()
|
||||
{
|
||||
var message = "Pandoc is open-source and free of charge, but if you reject Pandoc's license, it can not be installed and some of AIStudios data retrieval features will be disabled (e.g. using Office files like Word)." +
|
||||
"This decision can be revoked at any time. Are you sure you want to reject the license?";
|
||||
|
||||
var dialogParameters = new DialogParameters
|
||||
{
|
||||
{ "Message", message },
|
||||
};
|
||||
|
||||
var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>("Reject Pandoc's licence", dialogParameters, DialogOptions.FULLSCREEN);
|
||||
var dialogResult = await dialogReference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
dialogReference.Close();
|
||||
else
|
||||
this.Cancel();
|
||||
}
|
||||
|
||||
private string FilenameFromUri(string uri)
|
||||
{
|
||||
var index = uri.LastIndexOf('/');
|
||||
return uri[(index + 1)..];
|
||||
}
|
||||
|
||||
private async Task OnExpandedChanged(bool isExpanded)
|
||||
{
|
||||
if (isExpanded)
|
||||
{
|
||||
this.isLoading = true;
|
||||
try
|
||||
{
|
||||
await Task.Delay(600);
|
||||
|
||||
this.licenseText = await this.LoadLicenseTextAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.licenseText = "Error loading license text, please consider following the links to read the GPL.";
|
||||
LOG.LogError("Error loading GPL license text:\n{ErrorMessage}", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(350);
|
||||
this.licenseText = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> LoadLicenseTextAsync()
|
||||
{
|
||||
var response = await this.HttpClient.GetAsync(LICENCE_URI);
|
||||
response.EnsureSuccessStatusCode();
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
return content;
|
||||
}
|
||||
}
|
@ -90,7 +90,8 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
this.MessageBus.ApplyFilters(this, [],
|
||||
[
|
||||
Event.UPDATE_AVAILABLE, Event.CONFIGURATION_CHANGED, Event.COLOR_THEME_CHANGED, Event.SHOW_ERROR,
|
||||
Event.STARTUP_PLUGIN_SYSTEM, Event.PLUGINS_RELOADED
|
||||
Event.SHOW_ERROR, Event.SHOW_WARNING, Event.SHOW_SUCCESS, Event.STARTUP_PLUGIN_SYSTEM,
|
||||
Event.PLUGINS_RELOADED
|
||||
]);
|
||||
|
||||
// Set the snackbar for the update service:
|
||||
@ -176,12 +177,24 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan
|
||||
this.StateHasChanged();
|
||||
break;
|
||||
|
||||
case Event.SHOW_SUCCESS:
|
||||
if (data is DataSuccessMessage success)
|
||||
success.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_ERROR:
|
||||
if (data is Error error)
|
||||
if (data is DataErrorMessage error)
|
||||
error.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.SHOW_WARNING:
|
||||
if (data is DataWarningMessage warning)
|
||||
warning.Show(this.Snackbar);
|
||||
|
||||
break;
|
||||
|
||||
case Event.STARTUP_PLUGIN_SYSTEM:
|
||||
if(PreviewFeatures.PRE_PLUGINS_2025.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
|
@ -1,6 +1,6 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct Error(string Icon, string Message)
|
||||
public readonly record struct DataErrorMessage(string Icon, string Message)
|
||||
{
|
||||
public void Show(ISnackbar snackbar)
|
||||
{
|
16
app/MindWork AI Studio/Tools/DataSuccessMessage.cs
Normal file
16
app/MindWork AI Studio/Tools/DataSuccessMessage.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct DataSuccessMessage(string Icon, string Message)
|
||||
{
|
||||
public void Show(ISnackbar snackbar)
|
||||
{
|
||||
var icon = this.Icon;
|
||||
snackbar.Add(this.Message, Severity.Success, config =>
|
||||
{
|
||||
config.Icon = icon;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 10_000;
|
||||
});
|
||||
}
|
||||
}
|
16
app/MindWork AI Studio/Tools/DataWarningMessage.cs
Normal file
16
app/MindWork AI Studio/Tools/DataWarningMessage.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public readonly record struct DataWarningMessage(string Icon, string Message)
|
||||
{
|
||||
public void Show(ISnackbar snackbar)
|
||||
{
|
||||
var icon = this.Icon;
|
||||
snackbar.Add(this.Message, Severity.Warning, config =>
|
||||
{
|
||||
config.Icon = icon;
|
||||
config.IconSize = Size.Large;
|
||||
config.HideTransitionDuration = 600;
|
||||
config.VisibleStateDuration = 12_000;
|
||||
});
|
||||
}
|
||||
}
|
@ -11,6 +11,8 @@ public enum Event
|
||||
STARTUP_PLUGIN_SYSTEM,
|
||||
PLUGINS_RELOADED,
|
||||
SHOW_ERROR,
|
||||
SHOW_WARNING,
|
||||
SHOW_SUCCESS,
|
||||
|
||||
// Update events:
|
||||
USER_SEARCH_FOR_UPDATE,
|
||||
|
@ -72,7 +72,11 @@ public sealed class MessageBus
|
||||
}
|
||||
}
|
||||
|
||||
public Task SendError(Error error) => this.SendMessage(null, Event.SHOW_ERROR, error);
|
||||
public Task SendError(DataErrorMessage dataErrorMessage) => this.SendMessage(null, Event.SHOW_ERROR, dataErrorMessage);
|
||||
|
||||
public Task SendWarning(DataWarningMessage dataWarningMessage) => this.SendMessage(null, Event.SHOW_WARNING, dataWarningMessage);
|
||||
|
||||
public Task SendSuccess(DataSuccessMessage dataSuccessMessage) => this.SendMessage(null, Event.SHOW_SUCCESS, dataSuccessMessage);
|
||||
|
||||
public void DeferMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data = default)
|
||||
{
|
||||
|
284
app/MindWork AI Studio/Tools/Pandoc.cs
Normal file
284
app/MindWork AI Studio/Tools/Pandoc.cs
Normal file
@ -0,0 +1,284 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO.Compression;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public static partial class Pandoc
|
||||
{
|
||||
private const string CPU_ARCHITECTURE = "win-x64";
|
||||
private const string DOWNLOAD_URL = "https://github.com/jgm/pandoc/releases/download";
|
||||
private const string LATEST_URL = "https://github.com/jgm/pandoc/releases/latest";
|
||||
|
||||
private static readonly ILogger LOG = Program.LOGGER_FACTORY.CreateLogger("PandocService");
|
||||
private static readonly Version MINIMUM_REQUIRED_VERSION = new (3, 6);
|
||||
private static readonly Version FALLBACK_VERSION = new (3, 7, 0, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if pandoc is available on the system and can be started as a process or present in AiStudio's data dir
|
||||
/// </summary>
|
||||
/// <param name="rustService">Global rust service to access file system and data dir</param>
|
||||
/// <param name="showMessages">Controls if snackbars are shown to the user</param>
|
||||
/// <returns>True, if pandoc is available and the minimum required version is met, else False.</returns>
|
||||
public static async Task<bool> CheckAvailabilityAsync(RustService rustService, bool showMessages = true)
|
||||
{
|
||||
var installDir = await GetPandocDataFolder(rustService);
|
||||
var subdirectories = Directory.GetDirectories(installDir);
|
||||
|
||||
if (subdirectories.Length > 1)
|
||||
{
|
||||
await InstallAsync(rustService);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (HasPandoc(installDir)) return true;
|
||||
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = GetPandocExecutableName(),
|
||||
Arguments = "--version",
|
||||
RedirectStandardOutput = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
using var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
{
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Help, "The pandoc process could not be started."));
|
||||
LOG.LogInformation("The pandoc process was not started, it was null");
|
||||
return false;
|
||||
}
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
await process.WaitForExitAsync();
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, $"The pandoc process exited unexpectedly."));
|
||||
LOG.LogError("The pandoc process was exited with code {ProcessExitCode}", process.ExitCode);
|
||||
return false;
|
||||
}
|
||||
|
||||
var versionMatch = PandocCmdRegex().Match(output);
|
||||
if (!versionMatch.Success)
|
||||
{
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Terminal, $"pandoc --version returned an invalid format."));
|
||||
LOG.LogError("pandoc --version returned an invalid format:\n {Output}", output);
|
||||
return false;
|
||||
}
|
||||
var versions = versionMatch.Groups[1].Value;
|
||||
var installedVersion = Version.Parse(versions);
|
||||
|
||||
if (installedVersion >= MINIMUM_REQUIRED_VERSION)
|
||||
{
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, $"Pandoc {installedVersion.ToString()} is installed."));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Build, $"Pandoc {installedVersion.ToString()} is installed, but it doesn't match the required version ({MINIMUM_REQUIRED_VERSION.ToString()})."));
|
||||
LOG.LogInformation("Pandoc {Installed} is installed, but it does not match the required version ({Requirement})", installedVersion.ToString(), MINIMUM_REQUIRED_VERSION.ToString());
|
||||
return false;
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (showMessages)
|
||||
await MessageBus.INSTANCE.SendError(new (@Icons.Material.Filled.AppsOutage, "Pandoc is not installed."));
|
||||
LOG.LogError("Pandoc is not installed and threw an exception:\n {Message}", e.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasPandoc(string pandocDirectory)
|
||||
{
|
||||
try
|
||||
{
|
||||
var subdirectories = Directory.GetDirectories(pandocDirectory);
|
||||
|
||||
foreach (var subdirectory in subdirectories)
|
||||
{
|
||||
var pandocPath = Path.Combine(subdirectory, "pandoc.exe");
|
||||
if (File.Exists(pandocPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogInformation("Pandoc is not installed in the data directory and might have thrown and error:\n{ErrorMessage}", ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatically decompresses the latest pandoc archive into AiStudio's data directory
|
||||
/// </summary>
|
||||
/// <param name="rustService">Global rust service to access file system and data dir</param>
|
||||
/// <returns>None</returns>
|
||||
public static async Task InstallAsync(RustService rustService)
|
||||
{
|
||||
var installDir = await GetPandocDataFolder(rustService);
|
||||
ClearFolder(installDir);
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(installDir))
|
||||
Directory.CreateDirectory(installDir);
|
||||
|
||||
using var client = new HttpClient();
|
||||
var uri = await GenerateUriAsync();
|
||||
|
||||
var response = await client.GetAsync(uri);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, $"Pandoc was not installed successfully, because the download archive was not found."));
|
||||
LOG.LogError("Pandoc was not installed, the release archive was not found (Status Code {StatusCode}):\n{Uri}\n{Message}", response.StatusCode, uri, response.RequestMessage);
|
||||
return;
|
||||
}
|
||||
var fileBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
if (uri.Contains(".zip"))
|
||||
{
|
||||
var tempZipPath = Path.Join(Path.GetTempPath(), "pandoc.zip");
|
||||
await File.WriteAllBytesAsync(tempZipPath, fileBytes);
|
||||
ZipFile.ExtractToDirectory(tempZipPath, installDir);
|
||||
File.Delete(tempZipPath);
|
||||
}
|
||||
else if (uri.Contains(".tar.gz"))
|
||||
{
|
||||
var tempTarPath = Path.Join(Path.GetTempPath(), "pandoc.tar.gz");
|
||||
await File.WriteAllBytesAsync(tempTarPath, fileBytes);
|
||||
ZipFile.ExtractToDirectory(tempTarPath, installDir);
|
||||
File.Delete(tempTarPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, $"Pandoc was not installed successfully, because the download archive type is unknown."));
|
||||
LOG.LogError("Pandoc was not installed, the download archive is unknown:\n {Uri}", uri);
|
||||
return;
|
||||
}
|
||||
|
||||
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle,
|
||||
$"Pandoc {await FetchLatestVersionAsync()} was installed successfully."));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Fehler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearFolder(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) return;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var dir in Directory.GetDirectories(path))
|
||||
{
|
||||
Directory.Delete(dir, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LOG.LogError(ex, "Error clearing pandoc folder.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously fetch the content from Pandoc's latest release page and extract the latest version number
|
||||
/// </summary>
|
||||
/// <remarks>Version numbers can have the following formats: x.x, x.x.x or x.x.x.x</remarks>
|
||||
/// <returns>Latest Pandoc version number</returns>
|
||||
public static async Task<string> FetchLatestVersionAsync() {
|
||||
using var client = new HttpClient();
|
||||
var response = await client.GetAsync(LATEST_URL);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page:\n {Response}", response.StatusCode, response.RequestMessage);
|
||||
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, $"The latest pandoc version was not found, installing version {FALLBACK_VERSION.ToString()} instead."));
|
||||
return FALLBACK_VERSION.ToString();
|
||||
}
|
||||
|
||||
var htmlContent = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var versionMatch = LatestVersionRegex().Match(htmlContent);
|
||||
if (!versionMatch.Success)
|
||||
{
|
||||
LOG.LogError("The latest version regex returned nothing:\n {Value}", versionMatch.Groups.ToString());
|
||||
await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, $"The latest pandoc version was not found, installing version {FALLBACK_VERSION.ToString()} instead."));
|
||||
return FALLBACK_VERSION.ToString();
|
||||
}
|
||||
|
||||
var version = versionMatch.Groups[1].Value;
|
||||
return version;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the systems architecture to find the correct archive
|
||||
/// </summary>
|
||||
/// <returns>Full URI to the right archive in Pandoc's repo</returns>
|
||||
public static async Task<string> GenerateUriAsync()
|
||||
{
|
||||
var version = await FetchLatestVersionAsync();
|
||||
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
|
||||
return CPU_ARCHITECTURE switch
|
||||
{
|
||||
"win-x64" => $"{baseUri}windows-x86_64.zip",
|
||||
"osx-x64" => $"{baseUri}x86_64-macOS.zip",
|
||||
"osx-arm64" => $"{baseUri}arm64-macOS.zip",
|
||||
"linux-x64" => $"{baseUri}linux-amd64.tar.gz",
|
||||
"linux-arm" => $"{baseUri}linux-arm64.tar.gz",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the systems architecture to find the correct Pandoc installer
|
||||
/// </summary>
|
||||
/// <returns>Full URI to the right installer in Pandoc's repo</returns>
|
||||
public static async Task<string> GenerateInstallerUriAsync()
|
||||
{
|
||||
var version = await FetchLatestVersionAsync();
|
||||
var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-";
|
||||
|
||||
switch (CPU_ARCHITECTURE)
|
||||
{
|
||||
case "win-x64":
|
||||
return $"{baseUri}windows-x86_64.msi";
|
||||
case "osx-x64":
|
||||
return $"{baseUri}x86_64-macOS.pkg";
|
||||
case "osx-arm64":
|
||||
return $"{baseUri}arm64-macOS.pkg\n";
|
||||
default:
|
||||
await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Terminal, $"Installers are not available on {CPU_ARCHITECTURE} systems."));
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the os platform to determine the used executable name
|
||||
/// </summary>
|
||||
/// <returns>Name of the pandoc executable</returns>
|
||||
private static string GetPandocExecutableName() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "pandoc.exe" : "pandoc";
|
||||
|
||||
private static async Task<string> GetPandocDataFolder(RustService rustService) => Path.Join(await rustService.GetDataDirectory(), "pandoc");
|
||||
|
||||
[GeneratedRegex(@"pandoc(?:\.exe)?\s*([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:\.[0-9]+)?)")]
|
||||
private static partial Regex PandocCmdRegex();
|
||||
|
||||
[GeneratedRegex(@"pandoc(?:\.exe)?\s*([0-9]+\.[0-9]+(?:\.[0-9]+)?(?:\.[0-9]+)?)")]
|
||||
private static partial Regex LatestVersionRegex();
|
||||
}
|
@ -109,4 +109,35 @@
|
||||
/* Fixed the slider part of MudSplitter inside context div for inner scrolling component */
|
||||
.inner-scrolling-context > .mud-splitter > .mud-slider > .mud-slider-container {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background-color: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
border-radius: 6px !important;
|
||||
overflow: auto !important;
|
||||
font-family: Consolas, "Courier New", monospace !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.code-block pre {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.code-block code {
|
||||
font-family: inherit !important;
|
||||
}
|
||||
|
||||
.inline-code-block {
|
||||
background-color: #2d2d2d;
|
||||
color: #f8f8f2;
|
||||
border-radius: 6px;
|
||||
font-family: Consolas, "Courier New", monospace;
|
||||
text-align: left;
|
||||
padding: 4px 6px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.no-elevation {
|
||||
box-shadow: none !important;
|
||||
}
|
@ -25,4 +25,14 @@ window.clearDiv = function (divName) {
|
||||
|
||||
window.scrollToBottom = function(element) {
|
||||
element.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
|
||||
}
|
||||
|
||||
window.triggerDownload = function(url, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.setAttribute('download', filename);
|
||||
a.style.display = 'none';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
}
|
Loading…
Reference in New Issue
Block a user