diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor b/app/MindWork AI Studio/Components/ChatComponent.razor
index 983f1f45..6a8e4ad9 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor
@@ -82,8 +82,11 @@
}
-
-
+
+ @if (this.isPandocAvailable)
+ {
+
+ }
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{
diff --git a/app/MindWork AI Studio/Components/ChatComponent.razor.cs b/app/MindWork AI Studio/Components/ChatComponent.razor.cs
index b85229aa..502e6966 100644
--- a/app/MindWork AI Studio/Components/ChatComponent.razor.cs
+++ b/app/MindWork AI Studio/Components/ChatComponent.razor.cs
@@ -3,6 +3,7 @@ using AIStudio.Dialogs;
using AIStudio.Provider;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@@ -33,9 +34,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject]
private ILogger Logger { get; set; } = null!;
-
+
[Inject]
private IDialogService DialogService { get; init; } = null!;
+
+ [Inject]
+ private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary USER_INPUT_ATTRIBUTES = new();
@@ -58,6 +62,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private Guid currentWorkspaceId = Guid.Empty;
private CancellationTokenSource? cancellationTokenSource;
private HashSet chatDocumentPaths = [];
+ private bool isPandocAvailable;
// Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field.
@@ -198,6 +203,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Select the correct provider:
await this.SelectProviderWhenLoadingChat();
+
+ // Check if Pandoc is available (no dialog or messages):
+ this.isPandocAvailable = await this.PandocAvailabilityService.IsAvailableAsync();
+
await base.OnInitializedAsync();
}
diff --git a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
index c4019fa2..150acd09 100644
--- a/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
+++ b/app/MindWork AI Studio/Components/ReadFileContent.razor.cs
@@ -1,11 +1,8 @@
-using AIStudio.Dialogs;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
-using DialogOptions = AIStudio.Dialogs.DialogOptions;
-
namespace AIStudio.Components;
public partial class ReadFileContent : MSGComponentBase
@@ -21,12 +18,15 @@ public partial class ReadFileContent : MSGComponentBase
[Inject]
private RustService RustService { get; init; } = null!;
-
+
[Inject]
private IDialogService DialogService { get; init; } = null!;
-
+
[Inject]
private ILogger Logger { get; init; } = null!;
+
+ [Inject]
+ private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!;
private async Task SelectFile()
{
@@ -57,24 +57,9 @@ public partial class ReadFileContent : MSGComponentBase
}
// Ensure that Pandoc is installed and ready:
- var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showSuccessMessage: false);
- if (!pandocState.IsAvailable)
- {
- var dialogParameters = new DialogParameters
- {
- { x => x.ShowInitialResultInSnackbar, false },
- };
-
- var dialogReference = await this.DialogService.ShowAsync(T("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN);
- await dialogReference.Result;
-
- pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showSuccessMessage: true);
- if (!pandocState.IsAvailable)
- {
- this.Logger.LogError("Pandoc is not available after installation attempt.");
- await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, T("Pandoc may be required for importing files.")));
- }
- }
+ await this.PandocAvailabilityService.EnsureAvailabilityAsync(
+ showSuccessMessage: false,
+ showDialog: true);
var fileContent = await this.RustService.ReadArbitraryFileData(selectedFile.SelectedFilePath, int.MaxValue);
await this.FileContentChanged.InvokeAsync(fileContent);
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 2514a67f..b5954efc 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -126,6 +126,7 @@ internal sealed class Program
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddScoped();
builder.Services.AddTransient();
builder.Services.AddTransient();
builder.Services.AddTransient();
diff --git a/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs
new file mode 100644
index 00000000..14a26908
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/PandocAvailabilityService.cs
@@ -0,0 +1,85 @@
+using AIStudio.Dialogs;
+using AIStudio.Tools.PluginSystem;
+
+using DialogOptions = AIStudio.Dialogs.DialogOptions;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// Service to check Pandoc availability and ensure installation.
+/// This service encapsulates the logic for checking if Pandoc is installed
+/// and showing the installation dialog if needed.
+///
+public sealed class PandocAvailabilityService(RustService rustService, IDialogService dialogService, ILogger logger)
+{
+ private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PandocAvailabilityService).Namespace, nameof(PandocAvailabilityService));
+
+ private RustService RustService => rustService;
+
+ private IDialogService DialogService => dialogService;
+
+ private ILogger Logger => logger;
+
+ private PandocInstallation? cachedInstallation;
+
+ ///
+ /// Checks if Pandoc is available and shows the installation dialog if needed.
+ ///
+ /// Whether to show a success message if Pandoc is available.
+ /// Whether to show the installation dialog if Pandoc is not available.
+ /// The Pandoc installation state.
+ public async Task EnsureAvailabilityAsync(bool showSuccessMessage = false, bool showDialog = true)
+ {
+ // Check if Pandoc is available:
+ var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: false, showSuccessMessage: showSuccessMessage);
+
+ // Cache the result:
+ this.cachedInstallation = pandocState;
+
+ // If not available, show installation dialog:
+ if (!pandocState.IsAvailable && showDialog)
+ {
+ var dialogParameters = new DialogParameters
+ {
+ { x => x.ShowInitialResultInSnackbar, false },
+ };
+
+ var dialogReference = await this.DialogService.ShowAsync(TB("Pandoc Installation"), dialogParameters, DialogOptions.FULLSCREEN);
+ await dialogReference.Result;
+
+ // Re-check availability after dialog:
+ pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: showSuccessMessage, showSuccessMessage: showSuccessMessage);
+ this.cachedInstallation = pandocState;
+
+ if (!pandocState.IsAvailable)
+ {
+ this.Logger.LogError("Pandoc is not available after installation attempt.");
+ await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Pandoc may be required for importing files.")));
+ }
+ }
+
+ return pandocState;
+ }
+
+ ///
+ /// Checks if Pandoc is available without showing any dialogs or messages.
+ /// Uses cached result if available to avoid redundant checks.
+ ///
+ /// True if Pandoc is available, false otherwise.
+ public async Task IsAvailableAsync()
+ {
+ if (this.cachedInstallation.HasValue)
+ return this.cachedInstallation.Value.IsAvailable;
+
+ var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showMessages: false, showSuccessMessage: false);
+ this.cachedInstallation = pandocState;
+
+ return pandocState.IsAvailable;
+ }
+
+ ///
+ /// Clears the cached Pandoc installation state.
+ /// Useful when the installation state might have changed.
+ ///
+ public void ClearCache() => this.cachedInstallation = null;
+}