Merge branch 'main' into provider_expert_mode

This commit is contained in:
Thorsten Sommer 2025-11-13 18:12:45 +01:00 committed by GitHub
commit 3b70f1535b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 272 additions and 12 deletions

View File

@ -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"
@ -2974,6 +2977,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is dist
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
-- Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1717545317"] = "Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task."
-- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1847791252"] = "Update"
@ -5365,6 +5371,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."

View File

@ -2,6 +2,7 @@
@using MudBlazor
@using AIStudio.Components
@inherits AIStudio.Components.MSGComponentBase
<MudCard Class="@this.CardClasses" Outlined="@true">
<MudCardHeader>
<CardHeaderAvatar>
@ -47,6 +48,13 @@
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@this.RemoveBlock"/>
</MudTooltip>
}
@if (this.Role is ChatRole.AI)
{
<MudTooltip Text="@T("Export Chat to Microsoft Word")" Placement="Placement.Bottom">
<MudIconButton Icon="@Icons.Material.Filled.Save" OnClick="@this.ExportToWord"/>
</MudTooltip>
}
<MudCopyClipboardButton Content="@this.Content" Type="@this.Type" Size="Size.Medium"/>
</CardHeaderActions>
</MudCardHeader>

View File

@ -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);
}
}

View File

@ -42,8 +42,6 @@
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="444"
Counter="444"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI something about yourself. What is your profession? How experienced are you in this profession? Which technologies do you like?")"
@ -61,13 +59,15 @@
Lines="6"
AutoGrow="@true"
MaxLines="12"
MaxLength="256"
Counter="256"
Class="mb-3"
UserAttributes="@SPELLCHECK_ATTRIBUTES"
HelperText="@T("Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.")"
/>
<MudJustifiedText Typo="Typo.body2" Class="mb-3">
@T("Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.")
</MudJustifiedText>
</MudForm>
<Issues IssuesData="@this.dataIssues"/>
</DialogContent>

View File

@ -129,9 +129,6 @@ public partial class ProfileDialog : MSGComponentBase
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take.");
if(text.Length > 444)
return T("The text must not exceed 444 characters.");
return null;
}
@ -140,9 +137,6 @@ public partial class ProfileDialog : MSGComponentBase
if (string.IsNullOrWhiteSpace(this.DataNeedToKnow) && string.IsNullOrWhiteSpace(this.DataActions))
return T("Please enter what the LLM should know about you and/or what actions it should take.");
if(text.Length > 256)
return T("The text must not exceed 256 characters.");
return null;
}

View File

@ -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"
@ -2976,6 +2979,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc wird un
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Teilen Sie der KI mit, was sie machen soll. Was sind ihre Ziele oder was möchten Sie erreichen? Zum Beispiel, dass die KI Sie duzt."
-- Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1717545317"] = "Bitte beachten Sie, dass Ihre Profilinformationen Teil des System-Prompts werden. Das bedeutet, sie belegen einen Teil des Kontexts den „Speicher“, den das LLM nutzt, um Ihre Anfrage zu verstehen und darauf zu antworten. Wenn Ihr Profil extrem lang ist, kann das LLM Schwierigkeiten haben, die Aufgabe auszuführen."
-- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1847791252"] = "Aktualisieren"
@ -5367,6 +5373,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."
@ -5768,3 +5780,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unbenannt
-- Delete Chat
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Chat löschen"

View File

@ -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"
@ -2976,6 +2979,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PANDOCDIALOG::T523908375"] = "Pandoc is dist
-- Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1458195391"] = "Tell the AI what you want it to do for you. What are your goals or are you trying to achieve? Like having the AI address you informally."
-- Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1717545317"] = "Please be aware that your profile info becomes part of the system prompt. This means it uses up context space — the “memory” the LLM uses to understand and respond to your request. If your profile is extremely long, the LLM may struggle to focus on your actual task."
-- Update
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::PROFILEDIALOG::T1847791252"] = "Update"
@ -5367,6 +5373,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."
@ -5768,3 +5780,4 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T1307384014"] = "Unnamed w
-- Delete Chat
UI_TEXT_CONTENT["AISTUDIO::TOOLS::WORKSPACEBEHAVIOUR::T2244038752"] = "Delete Chat"

View File

@ -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<bool> 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}'");
}
}
}
}

View File

@ -19,6 +19,7 @@ public sealed class PandocProcessBuilder
private string? providedOutputFile;
private string? providedInputFormat;
private string? providedOutputFormat;
private bool useStandaloneMode;
private readonly List<string> 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<PandocPreparedProcess> BuildAsync(RustService rustService)
{
var sbArguments = new StringBuilder();
if (this.useStandaloneMode)
sbArguments.Append(" --standalone ");
if(!string.IsNullOrWhiteSpace(this.providedInputFile))
sbArguments.Append(this.providedInputFile);

View File

@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct FileSaveResponse(bool UserCancelled, string SaveFilePath);

View File

@ -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; }
}

View File

@ -35,4 +35,31 @@ public sealed partial class RustService
return await result.Content.ReadFromJsonAsync<FileSelectionResponse>(this.jsonRustSerializerOptions);
}
/// <summary>
/// Initiates a dialog to let the user select a file for a writing operation.
/// </summary>
/// <param name="title">The title of the save file dialog.</param>
/// <param name="filter">An optional file type filter for filtering specific file formats.</param>
/// <param name="initialFile">An optional initial file path to pre-fill in the dialog.</param>
/// <returns>A <see cref="FileSaveResponse"/> object containing information about whether the user canceled the
/// operation and whether the select operation was successful.</returns>
public async Task<FileSaveResponse> 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<FileSaveResponse>(this.jsonRustSerializerOptions);
}
}

View File

@ -1,2 +1,4 @@
# v0.9.53, build 228 (2025-10-xx xx:xx UTC)
- Added expert settings to the provider dialog to enable setting additional parameters. Also, additional parameters can be configured by configuration plugins for enterprise scenarios. Thanks to Peer (`peerschuett`) for this contribution.
- Added expert settings to the provider dialog to enable setting additional parameters. Also, additional parameters can be configured by configuration plugins for enterprise scenarios. Thanks to Peer (`peerschuett`) for this contribution.
- Added the ability to export AI responses from the chat into Microsoft Word files. Thank you, Sabrina (`Sabrina-devops`), for your first contribution.
- Removed the character limit for profiles.

View File

@ -306,6 +306,13 @@ pub struct SelectFileOptions {
filter: Option<FileTypeFilter>,
}
#[derive(Clone, Deserialize)]
pub struct SaveFileOptions {
title: String,
name_file: Option<PreviousFile>,
filter: Option<FileTypeFilter>,
}
#[derive(Serialize)]
pub struct DirectorySelectionResponse {
user_cancelled: bool,
@ -362,6 +369,55 @@ pub fn select_file(_token: APIToken, payload: Json<SelectFileOptions>) -> Json<F
}
}
#[post("/save/file", data = "<payload>")]
pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> {
// 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::<Vec<&str>>())
},
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/");

View File

@ -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,