Inject PandocAvailabilityService and refactor its usage

This commit is contained in:
Thorsten Sommer 2025-12-10 13:27:24 +01:00
parent 85e0d7dd6c
commit bb843fa812
Signed by: tsommer
GPG Key ID: 371BBA77A02C0108
5 changed files with 109 additions and 26 deletions

View File

@ -82,8 +82,11 @@
} }
<ChatTemplateSelection CanChatThreadBeUsedForTemplate="@this.CanThreadBeSaved" CurrentChatThread="@this.ChatThread" CurrentChatTemplate="@this.currentChatTemplate" CurrentChatTemplateChanged="@this.ChatTemplateWasChanged"/> <ChatTemplateSelection CanChatThreadBeUsedForTemplate="@this.CanThreadBeSaved" CurrentChatThread="@this.ChatThread" CurrentChatTemplate="@this.currentChatTemplate" CurrentChatTemplateChanged="@this.ChatTemplateWasChanged"/>
<AttachDocuments Name="File Attachments" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true"/> @if (this.isPandocAvailable)
{
<AttachDocuments Name="File Attachments" @bind-DocumentPaths="@this.chatDocumentPaths" CatchAllDocuments="true" UseSmallForm="true"/>
}
@if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY) @if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
{ {

View File

@ -3,6 +3,7 @@ using AIStudio.Dialogs;
using AIStudio.Provider; using AIStudio.Provider;
using AIStudio.Settings; using AIStudio.Settings;
using AIStudio.Settings.DataModel; using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
@ -33,9 +34,12 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
[Inject] [Inject]
private ILogger<ChatComponent> Logger { get; set; } = null!; private ILogger<ChatComponent> Logger { get; set; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject]
private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!;
private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top; private const Placement TOOLBAR_TOOLTIP_PLACEMENT = Placement.Top;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new(); private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
@ -58,6 +62,7 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
private Guid currentWorkspaceId = Guid.Empty; private Guid currentWorkspaceId = Guid.Empty;
private CancellationTokenSource? cancellationTokenSource; private CancellationTokenSource? cancellationTokenSource;
private HashSet<string> chatDocumentPaths = []; private HashSet<string> chatDocumentPaths = [];
private bool isPandocAvailable;
// Unfortunately, we need the input field reference to blur the focus away. Without // Unfortunately, we need the input field reference to blur the focus away. Without
// this, we cannot clear the input field. // this, we cannot clear the input field.
@ -198,6 +203,10 @@ public partial class ChatComponent : MSGComponentBase, IAsyncDisposable
// Select the correct provider: // Select the correct provider:
await this.SelectProviderWhenLoadingChat(); await this.SelectProviderWhenLoadingChat();
// Check if Pandoc is available (no dialog or messages):
this.isPandocAvailable = await this.PandocAvailabilityService.IsAvailableAsync();
await base.OnInitializedAsync(); await base.OnInitializedAsync();
} }

View File

@ -1,11 +1,8 @@
using AIStudio.Dialogs;
using AIStudio.Tools.Rust; using AIStudio.Tools.Rust;
using AIStudio.Tools.Services; using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components; namespace AIStudio.Components;
public partial class ReadFileContent : MSGComponentBase public partial class ReadFileContent : MSGComponentBase
@ -21,12 +18,15 @@ public partial class ReadFileContent : MSGComponentBase
[Inject] [Inject]
private RustService RustService { get; init; } = null!; private RustService RustService { get; init; } = null!;
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
[Inject] [Inject]
private ILogger<ReadFileContent> Logger { get; init; } = null!; private ILogger<ReadFileContent> Logger { get; init; } = null!;
[Inject]
private PandocAvailabilityService PandocAvailabilityService { get; init; } = null!;
private async Task SelectFile() private async Task SelectFile()
{ {
@ -57,24 +57,9 @@ public partial class ReadFileContent : MSGComponentBase
} }
// Ensure that Pandoc is installed and ready: // Ensure that Pandoc is installed and ready:
var pandocState = await Pandoc.CheckAvailabilityAsync(this.RustService, showSuccessMessage: false); await this.PandocAvailabilityService.EnsureAvailabilityAsync(
if (!pandocState.IsAvailable) showSuccessMessage: false,
{ showDialog: true);
var dialogParameters = new DialogParameters<PandocDialog>
{
{ x => x.ShowInitialResultInSnackbar, false },
};
var dialogReference = await this.DialogService.ShowAsync<PandocDialog>(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.")));
}
}
var fileContent = await this.RustService.ReadArbitraryFileData(selectedFile.SelectedFilePath, int.MaxValue); var fileContent = await this.RustService.ReadArbitraryFileData(selectedFile.SelectedFilePath, int.MaxValue);
await this.FileContentChanged.InvokeAsync(fileContent); await this.FileContentChanged.InvokeAsync(fileContent);

View File

@ -126,6 +126,7 @@ internal sealed class Program
builder.Services.AddSingleton<SettingsManager>(); builder.Services.AddSingleton<SettingsManager>();
builder.Services.AddSingleton<ThreadSafeRandom>(); builder.Services.AddSingleton<ThreadSafeRandom>();
builder.Services.AddSingleton<DataSourceService>(); builder.Services.AddSingleton<DataSourceService>();
builder.Services.AddScoped<PandocAvailabilityService>();
builder.Services.AddTransient<HTMLParser>(); builder.Services.AddTransient<HTMLParser>();
builder.Services.AddTransient<AgentDataSourceSelection>(); builder.Services.AddTransient<AgentDataSourceSelection>();
builder.Services.AddTransient<AgentRetrievalContextValidation>(); builder.Services.AddTransient<AgentRetrievalContextValidation>();

View File

@ -0,0 +1,85 @@
using AIStudio.Dialogs;
using AIStudio.Tools.PluginSystem;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Tools.Services;
/// <summary>
/// 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.
/// </summary>
public sealed class PandocAvailabilityService(RustService rustService, IDialogService dialogService, ILogger<PandocAvailabilityService> 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<PandocAvailabilityService> Logger => logger;
private PandocInstallation? cachedInstallation;
/// <summary>
/// Checks if Pandoc is available and shows the installation dialog if needed.
/// </summary>
/// <param name="showSuccessMessage">Whether to show a success message if Pandoc is available.</param>
/// <param name="showDialog">Whether to show the installation dialog if Pandoc is not available.</param>
/// <returns>The Pandoc installation state.</returns>
public async Task<PandocInstallation> 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<PandocDialog>
{
{ x => x.ShowInitialResultInSnackbar, false },
};
var dialogReference = await this.DialogService.ShowAsync<PandocDialog>(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;
}
/// <summary>
/// Checks if Pandoc is available without showing any dialogs or messages.
/// Uses cached result if available to avoid redundant checks.
/// </summary>
/// <returns>True if Pandoc is available, false otherwise.</returns>
public async Task<bool> 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;
}
/// <summary>
/// Clears the cached Pandoc installation state.
/// Useful when the installation state might have changed.
/// </summary>
public void ClearCache() => this.cachedInstallation = null;
}