Skip to content

Commit 9418b99

Browse files
Added export to word function for the chat (#566)
1 parent 37bd42e commit 9418b99

File tree

12 files changed

+256
-1
lines changed

12 files changed

+256
-1
lines changed

app/MindWork AI Studio/Assistants/I18N/allTexts.lua

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
13631363
-- No, keep it
13641364
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
13651365

1366+
-- Export Chat to Microsoft Word
1367+
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
1368+
13661369
-- Open Settings
13671370
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings"
13681371

@@ -5350,6 +5353,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc i
53505353
-- The latest Pandoc version was not found, installing version {0} instead.
53515354
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
53525355

5356+
-- Error during Microsoft Word export
5357+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export"
5358+
5359+
-- Microsoft Word export successful
5360+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful"
5361+
53535362
-- The table AUTHORS does not exist or is using an invalid syntax.
53545363
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax."
53555364

app/MindWork AI Studio/Chat/ContentBlockComponent.razor

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
@using MudBlazor
33
@using AIStudio.Components
44
@inherits AIStudio.Components.MSGComponentBase
5+
56
<MudCard Class="@this.CardClasses" Outlined="@true">
67
<MudCardHeader>
78
<CardHeaderAvatar>
@@ -47,6 +48,13 @@
4748
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
4849
</MudTooltip>
4950
}
51+
52+
@if (this.Role is ChatRole.AI)
53+
{
54+
<MudTooltip Text="@T("Export Chat to Microsoft Word")" Placement="Placement.Bottom">
55+
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@this.ExportToWord"/>
56+
</MudTooltip>
57+
}
5058
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium"/>
5159
</CardHeaderActions>
5260
</MudCardHeader>

app/MindWork AI Studio/Chat/ContentBlockComponent.razor.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
using AIStudio.Components;
2-
2+
using AIStudio.Tools.Services;
33
using Microsoft.AspNetCore.Components;
44

55
namespace AIStudio.Chat;
@@ -63,6 +63,9 @@ public partial class ContentBlockComponent : MSGComponentBase
6363
[Inject]
6464
private IDialogService DialogService { get; init; } = null!;
6565

66+
[Inject]
67+
private RustService RustService { get; init; } = null!;
68+
6669
private bool HideContent { get; set; }
6770

6871
#region Overrides of ComponentBase
@@ -133,6 +136,11 @@ private async Task RemoveBlock()
133136
await this.RemoveBlockFunc(this.Content);
134137
}
135138

139+
private async Task ExportToWord()
140+
{
141+
await PandocExport.ToMicrosoftWord(this.RustService, T("Export Chat to Microsoft Word"), this.Content);
142+
}
143+
136144
private async Task RegenerateBlock()
137145
{
138146
if (this.RegenerateFunc is null)
@@ -179,4 +187,5 @@ private async Task EditLastUserBlock()
179187
if (edit.HasValue && edit.Value)
180188
await this.EditLastUserBlockFunc(this.Content);
181189
}
190+
182191
}

app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Nachric
13651365
-- No, keep it
13661366
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "Nein, behalten"
13671367

1368+
-- Export Chat to Microsoft Word
1369+
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Chat in Microsoft Word exportieren"
1370+
13681371
-- Open Settings
13691372
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Einstellungen öffnen"
13701373

@@ -5352,6 +5355,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pando
53525355
-- The latest Pandoc version was not found, installing version {0} instead.
53535356
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert."
53545357

5358+
-- Error during Microsoft Word export
5359+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Fehler beim Exportieren nach Microsoft Word"
5360+
5361+
-- Microsoft Word export successful
5362+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Export nach Microsoft Word erfolgreich"
5363+
53555364
-- The table AUTHORS does not exist or is using an invalid syntax.
53565365
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "Die Tabelle AUTHORS existiert nicht oder verwendet eine ungültige Syntax."
53575366

@@ -5753,3 +5762,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt
57535762

57545763
-- Delete Chat
57555764
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen"
5765+

app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1365,6 +1365,9 @@ UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4070211974"] = "Remove
13651365
-- No, keep it
13661366
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T4188329028"] = "No, keep it"
13671367

1368+
-- Export Chat to Microsoft Word
1369+
UI_TEXT_CONTENT["AISTUDIO::CHAT::CONTENTBLOCKCOMPONENT::T861873672"] = "Export Chat to Microsoft Word"
1370+
13681371
-- Open Settings
13691372
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::ASSISTANTBLOCK::T1172211894"] = "Open Settings"
13701373

@@ -5352,6 +5355,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc i
53525355
-- The latest Pandoc version was not found, installing version {0} instead.
53535356
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead."
53545357

5358+
-- Error during Microsoft Word export
5359+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T3290596792"] = "Error during Microsoft Word export"
5360+
5361+
-- Microsoft Word export successful
5362+
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T4256043333"] = "Microsoft Word export successful"
5363+
53555364
-- The table AUTHORS does not exist or is using an invalid syntax.
53565365
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax."
53575366

@@ -5753,3 +5762,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w
57535762

57545763
-- Delete Chat
57555764
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat"
5765+
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Diagnostics;
2+
using AIStudio.Chat;
3+
using AIStudio.Tools.PluginSystem;
4+
using AIStudio.Tools.Services;
5+
6+
namespace AIStudio.Tools;
7+
8+
public static class PandocExport
9+
{
10+
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(PandocExport));
11+
12+
private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(PandocExport).Namespace, nameof(PandocExport));
13+
14+
public static async Task<bool> ToMicrosoftWord(RustService rustService, string dialogTitle, IContent markdownContent)
15+
{
16+
var response = await rustService.SaveFile(dialogTitle, new("Microsoft Word", ["docx"]));
17+
if (response.UserCancelled)
18+
{
19+
LOGGER.LogInformation("User cancelled the save dialog.");
20+
return false;
21+
}
22+
23+
LOGGER.LogInformation($"The user chose the path '{response.SaveFilePath}' for the Microsoft Word export.");
24+
25+
var tempMarkdownFile = Guid.NewGuid().ToString();
26+
var tempMarkdownFilePath = Path.Combine(Path.GetTempPath(), tempMarkdownFile);
27+
28+
try
29+
{
30+
// Extract text content from chat:
31+
var markdownText = markdownContent switch
32+
{
33+
ContentText text => text.Text,
34+
ContentImage _ => "Image export to Microsoft Word not yet possible",
35+
36+
_ => "Unknown content type. Cannot export to Word."
37+
};
38+
39+
// Write text content to a temporary file:
40+
await File.WriteAllTextAsync(tempMarkdownFilePath, markdownText);
41+
42+
// Ensure that Pandoc is installed and ready:
43+
var pandocState = await Pandoc.CheckAvailabilityAsync(rustService);
44+
if (!pandocState.IsAvailable)
45+
return false;
46+
47+
// Call Pandoc to create the Word file:
48+
var pandoc = await PandocProcessBuilder
49+
.Create()
50+
.UseStandaloneMode()
51+
.WithInputFormat("markdown")
52+
.WithOutputFormat("docx")
53+
.WithOutputFile(response.SaveFilePath)
54+
.WithInputFile(tempMarkdownFilePath)
55+
.BuildAsync(rustService);
56+
57+
using var process = Process.Start(pandoc.StartInfo);
58+
if (process is null)
59+
{
60+
LOGGER.LogError("Failed to start Pandoc process.");
61+
return false;
62+
}
63+
64+
await process.WaitForExitAsync();
65+
if (process.ExitCode is not 0)
66+
{
67+
var error = await process.StandardError.ReadToEndAsync();
68+
LOGGER.LogError($"Pandoc failed with exit code {process.ExitCode}: {error}");
69+
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
70+
return false;
71+
}
72+
73+
LOGGER.LogInformation("Pandoc conversion successful.");
74+
await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, TB("Microsoft Word export successful")));
75+
76+
return true;
77+
}
78+
catch (Exception ex)
79+
{
80+
LOGGER.LogError(ex, "Error during Word export.");
81+
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Cancel, TB("Error during Microsoft Word export")));
82+
return false;
83+
}
84+
finally
85+
{
86+
// Try to remove the temp file:
87+
try
88+
{
89+
File.Delete(tempMarkdownFilePath);
90+
}
91+
catch
92+
{
93+
LOGGER.LogWarning($"Was not able to delete temporary file: '{tempMarkdownFilePath}'");
94+
}
95+
}
96+
}
97+
}

app/MindWork AI Studio/Tools/PandocProcessBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public sealed class PandocProcessBuilder
1919
private string? providedOutputFile;
2020
private string? providedInputFormat;
2121
private string? providedOutputFormat;
22+
private bool useStandaloneMode;
2223

2324
private readonly List<string> additionalArguments = new();
2425

@@ -57,10 +58,19 @@ public PandocProcessBuilder AddArgument(string argument)
5758
this.additionalArguments.Add(argument);
5859
return this;
5960
}
61+
62+
public PandocProcessBuilder UseStandaloneMode()
63+
{
64+
this.useStandaloneMode = true;
65+
return this;
66+
}
6067

6168
public async Task<PandocPreparedProcess> BuildAsync(RustService rustService)
6269
{
6370
var sbArguments = new StringBuilder();
71+
72+
if (this.useStandaloneMode)
73+
sbArguments.Append(" --standalone ");
6474

6575
if(!string.IsNullOrWhiteSpace(this.providedInputFile))
6676
sbArguments.Append(this.providedInputFile);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
namespace AIStudio.Tools.Rust;
2+
3+
public readonly record struct FileSaveResponse(bool UserCancelled, string SaveFilePath);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace AIStudio.Tools.Rust;
2+
3+
public class SaveFileOptions
4+
{
5+
public required string Title { get; init; }
6+
7+
public PreviousFile? PreviousFile { get; init; }
8+
9+
public FileTypeFilter? Filter { get; init; }
10+
}

app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,31 @@ public async Task<FileSelectionResponse> SelectFile(string title, FileTypeFilter
3535

3636
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
3737
}
38+
39+
/// <summary>
40+
/// Initiates a dialog to let the user select a file for a writing operation.
41+
/// </summary>
42+
/// <param name="title">The title of the save file dialog.</param>
43+
/// <param name="filter">An optional file type filter for filtering specific file formats.</param>
44+
/// <param name="initialFile">An optional initial file path to pre-fill in the dialog.</param>
45+
/// <returns>A <see cref="FileSaveResponse"/> object containing information about whether the user canceled the
46+
/// operation and whether the select operation was successful.</returns>
47+
public async Task<FileSaveResponse> SaveFile(string title, FileTypeFilter? filter = null, string? initialFile = null)
48+
{
49+
var payload = new SaveFileOptions
50+
{
51+
Title = title,
52+
PreviousFile = initialFile is null ? null : new (initialFile),
53+
Filter = filter
54+
};
55+
56+
var result = await this.http.PostAsJsonAsync("/save/file", payload, this.jsonRustSerializerOptions);
57+
if (!result.IsSuccessStatusCode)
58+
{
59+
this.logger!.LogError($"Failed to select a file for writing operation '{result.StatusCode}'");
60+
return new FileSaveResponse(true, string.Empty);
61+
}
62+
63+
return await result.Content.ReadFromJsonAsync<FileSaveResponse>(this.jsonRustSerializerOptions);
64+
}
3865
}

0 commit comments

Comments
 (0)