diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index d4572354..7e6c92f1 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -1363,6 +1363,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it" +-- Export Chat to Microsoft Word +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word" + -- Open Settings UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings" @@ -5350,6 +5353,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc i -- The latest Pandoc version was not found, installing version {0} instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead." +-- Error during Microsoft Word export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export" + +-- Microsoft Word export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" + -- The table AUTHORS does not exist or is using an invalid syntax. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax." diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor index fc73964c..164b07c0 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor @@ -2,6 +2,7 @@ @using MudBlazor @using AIStudio.Components @inherits AIStudio.Components.MSGComponentBase + @@ -47,6 +48,13 @@ } + + @if (this.Role is ChatRole.AI) + { + + + + } diff --git a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs index 1f5c86b7..13575faa 100644 --- a/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs +++ b/app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs @@ -1,5 +1,5 @@ using AIStudio.Components; - +using AIStudio.Tools.Services; using Microsoft.AspNetCore.Components; namespace AIStudio.Chat; @@ -63,6 +63,9 @@ public partial class ContentBlockComponent : MSGComponentBase [Inject] private IDialogService DialogService { get; init; } = null!; + [Inject] + private RustService RustService { get; init; } = null!; + private bool HideContent { get; set; } #region Overrides of ComponentBase @@ -133,6 +136,11 @@ public partial class ContentBlockComponent : MSGComponentBase await this.RemoveBlockFunc(this.Content); } + private async Task ExportToWord() + { + await PandocExport.ToMicrosoftWord(this.RustService, T("Export Chat to Microsoft Word"), this.Content); + } + private async Task RegenerateBlock() { if (this.RegenerateFunc is null) @@ -179,4 +187,5 @@ public partial class ContentBlockComponent : MSGComponentBase if (edit.HasValue && edit.Value) await this.EditLastUserBlockFunc(this.Content); } + } \ No newline at end of file diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 7e93a9dc..678e1bf9 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -1365,6 +1365,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachric -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "Nein, behalten" +-- Export Chat to Microsoft Word +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Chat in Microsoft Word exportieren" + -- Open Settings UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Einstellungen öffnen" @@ -5352,6 +5355,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pando -- The latest Pandoc version was not found, installing version {0} instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert." +-- Error during Microsoft Word export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Fehler beim Exportieren nach Microsoft Word" + +-- Microsoft Word export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Export nach Microsoft Word erfolgreich" + -- The table AUTHORS does not exist or is using an invalid syntax. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "Die Tabelle AUTHORS existiert nicht oder verwendet eine ungültige Syntax." @@ -5753,3 +5762,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen" + diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index f0a4d270..ce8b5bdd 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -1365,6 +1365,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove -- No, keep it UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it" +-- Export Chat to Microsoft Word +UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word" + -- Open Settings UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings" @@ -5352,6 +5355,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc i -- The latest Pandoc version was not found, installing version {0} instead. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead." +-- Error during Microsoft Word export +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export" + +-- Microsoft Word export successful +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful" + -- The table AUTHORS does not exist or is using an invalid syntax. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax." @@ -5753,3 +5762,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w -- Delete Chat UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat" + diff --git a/app/MindWork AI Studio/Tools/PandocExport.cs b/app/MindWork AI Studio/Tools/PandocExport.cs new file mode 100644 index 00000000..23c660d9 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PandocExport.cs @@ -0,0 +1,97 @@ +using System.Diagnostics; +using AIStudio.Chat; +using AIStudio.Tools.PluginSystem; +using AIStudio.Tools.Services; + +namespace AIStudio.Tools; + +public static class PandocExport +{ + private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport)); + + private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport)); + + public static async Task ToMicrosoftWord(RustService rustService, string dialogTitle, IContent markdownContent) + { + var response = await rustService.SaveFile(dialogTitle, new("Microsoft Word", ["docx"])); + if (response.UserCancelled) + { + LOGGER.LogInformation("User cancelled the save dialog."); + return false; + } + + LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the Microsoft Word export."); + + var tempMarkdownFile = Guid.NewGuid().ToString(); + var tempMarkdownFilePath = Path.Combine(Path.GetTempPath(), tempMarkdownFile); + + try + { + // Extract text content from chat: + var markdownText = markdownContent switch + { + ContentText text => text.Text, + ContentImage _ => "Image export to Microsoft Word not yet possible", + + _ => "Unknown content type. Cannot export to Word." + }; + + // Write text content to a temporary file: + await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText); + + // Ensure that Pandoc is installed and ready: + var pandocState = await Pandoc.CheckAvailabilityAsync(rustService); + if (!pandocState.IsAvailable) + return false; + + // Call Pandoc to create the Word file: + var pandoc = await PandocProcessBuilder + .Create() + .UseStandaloneMode() + .WithInputFormat("markdown") + .WithOutputFormat("docx") + .WithOutputFile(response.SaveFilePath) + .WithInputFile(tempMarkdownFilePath) + .BuildAsync(rustService); + + using var process = Process.Start(pandoc.StartInfo); + if (process is null) + { + LOGGER.LogError("Failed to start Pandoc process."); + return false; + } + + await process.WaitForExitAsync(); + if (process.ExitCode is not 0) + { + var error = await process.StandardError.ReadToEndAsync(); + LOGGER.LogError($"Pandoc failed with exit code {process.ExitCode}: {error}"); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export"))); + return false; + } + + LOGGER.LogInformation("Pandoc conversion successful."); + await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Microsoft Word export successful"))); + + return true; + } + catch (Exception ex) + { + LOGGER.LogError(ex, "Error during Word export."); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export"))); + return false; + } + finally + { + // Try to remove the temp file: + try + { + File.Delete(tempMarkdownFilePath); + } + catch + { + LOGGER.LogWarning($"Was not able to delete temporary file: '{tempMarkdownFilePath}'"); + } + } + } +} diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs index bd2184d0..90bfb468 100644 --- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs +++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs @@ -19,6 +19,7 @@ public sealed class PandocProcessBuilder private string? providedOutputFile; private string? providedInputFormat; private string? providedOutputFormat; + private bool useStandaloneMode; private readonly List additionalArguments = new(); @@ -57,10 +58,19 @@ public sealed class PandocProcessBuilder this.additionalArguments.Add(argument); return this; } + + public PandocProcessBuilder UseStandaloneMode() + { + this.useStandaloneMode = true; + return this; + } public async Task BuildAsync(RustService rustService) { var sbArguments = new StringBuilder(); + + if (this.useStandaloneMode) + sbArguments.Append(" --standalone "); if(!string.IsNullOrWhiteSpace(this.providedInputFile)) sbArguments.Append(this.providedInputFile); diff --git a/app/MindWork AI Studio/Tools/Rust/FileSaveResponse.cs b/app/MindWork AI Studio/Tools/Rust/FileSaveResponse.cs new file mode 100644 index 00000000..d6e8a7b1 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/FileSaveResponse.cs @@ -0,0 +1,3 @@ +namespace AIStudio.Tools.Rust; + +public readonly record struct FileSaveResponse(bool UserCancelled, string SaveFilePath); diff --git a/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs new file mode 100644 index 00000000..107e581a --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/SaveFileOptions.cs @@ -0,0 +1,10 @@ +namespace AIStudio.Tools.Rust; + +public class SaveFileOptions +{ + public required string Title { get; init; } + + public PreviousFile? PreviousFile { get; init; } + + public FileTypeFilter? Filter { get; init; } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index fc3e73bd..9fd670c0 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -35,4 +35,31 @@ public sealed partial class RustService return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); } + + /// + /// Initiates a dialog to let the user select a file for a writing operation. + /// + /// The title of the save file dialog. + /// An optional file type filter for filtering specific file formats. + /// An optional initial file path to pre-fill in the dialog. + /// A object containing information about whether the user canceled the + /// operation and whether the select operation was successful. + public async Task SaveFile(string title, FileTypeFilter? filter = null, string? initialFile = null) + { + var payload = new SaveFileOptions + { + Title = title, + PreviousFile = initialFile is null ? null : new (initialFile), + Filter = filter + }; + + var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to select a file for writing operation '{result.StatusCode}'"); + return new FileSaveResponse(true, string.Empty); + } + + return await result.Content.ReadFromJsonAsync(this.jsonRustSerializerOptions); + } } \ No newline at end of file diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index b551b0f0..25764123 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -306,6 +306,13 @@ pub struct SelectFileOptions { filter: Option, } +#[derive(Clone, Deserialize)] +pub struct SaveFileOptions { + title: String, + name_file: Option, + filter: Option, +} + #[derive(Serialize)] pub struct DirectorySelectionResponse { user_cancelled: bool, @@ -362,6 +369,55 @@ pub fn select_file(_token: APIToken, payload: Json) -> Json) -> Json { + + // Create a new file dialog builder: + let file_dialog = FileDialogBuilder::new(); + + // Set the title of the file dialog: + let file_dialog = file_dialog.set_title(&payload.title); + + // Set the file type filter if provided: + let file_dialog = match &payload.filter { + Some(filter) => { + file_dialog.add_filter(&filter.filter_name, &filter.filter_extensions.iter().map(|s| s.as_str()).collect::>()) + }, + + None => file_dialog, + }; + + // Set the previous file path if provided: + let file_dialog = match &payload.name_file { + Some(previous) => { + let previous_path = previous.file_path.as_str(); + file_dialog.set_directory(previous_path) + }, + + None => file_dialog, + }; + + // Displays the file dialogue box and select the file: + let file_path = file_dialog.save_file(); + match file_path { + Some(path) => { + info!("User selected file for writing operation: {path:?}"); + Json(FileSaveResponse { + user_cancelled: false, + save_file_path: path.to_str().unwrap().to_string(), + }) + }, + + None => { + info!("User cancelled file selection."); + Json(FileSaveResponse { + user_cancelled: true, + save_file_path: String::from(""), + }) + }, + } +} + #[derive(Clone, Deserialize)] pub struct PreviousFile { file_path: String, @@ -372,6 +428,11 @@ pub struct FileSelectionResponse { user_cancelled: bool, selected_file_path: String, } +#[derive(Serialize)] +pub struct FileSaveResponse { + user_cancelled: bool, + save_file_path: String, +} fn set_pdfium_path(path_resolver: PathResolver) { let pdfium_relative_source_path = String::from("resources/libraries/"); diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index b700af5b..f510b132 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -72,6 +72,7 @@ pub fn start_runtime_api() { crate::app_window::install_update, crate::app_window::select_directory, crate::app_window::select_file, + crate::app_window::save_file, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret,