From c08f9e2ea142d2aaf273440455e497d446970025 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Fri, 22 May 2026 15:46:03 +0200 Subject: [PATCH] Added support for exporting chat templates & profiles (#772) --- .../DocumentAnalysisAssistant.razor.cs | 14 +- .../Assistants/I18N/allTexts.lua | 36 +++ .../Dialogs/Settings/SettingsDialogBase.cs | 3 + .../Settings/SettingsDialogChatTemplate.razor | 22 ++ .../SettingsDialogChatTemplate.razor.cs | 62 +++++ .../SettingsDialogDataSources.razor.cs | 5 - .../Settings/SettingsDialogProfiles.razor | 6 + .../Settings/SettingsDialogProfiles.razor.cs | 13 + .../Plugins/configuration/plugin.lua | 3 +- .../plugin.lua | 39 +++ .../plugin.lua | 39 +++ .../Settings/ChatTemplate.cs | 236 +++++++++++++++++- .../Settings/DataModel/DataSourceERI_V1.cs | 3 +- .../Settings/EmbeddingProvider.cs | 3 +- app/MindWork AI Studio/Settings/Profile.cs | 21 +- app/MindWork AI Studio/Settings/Provider.cs | 3 +- .../Settings/TranscriptionProvider.cs | 3 +- app/MindWork AI Studio/Tools/LuaTools.cs | 16 -- .../Tools/PluginSystem/PluginConfiguration.cs | 2 +- .../PluginSystem/PluginConfigurationObject.cs | 6 +- .../PluginSystem/PluginFactory.Loading.cs | 6 +- .../Tools/Services/RustService.FileSystem.cs | 6 +- .../wwwroot/changelog/v26.5.5.md | 1 + app/SharedTools/LuaTools.cs | 44 +++- 24 files changed, 543 insertions(+), 49 deletions(-) delete mode 100644 app/MindWork AI Studio/Tools/LuaTools.cs diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs index 77522cd8..e7b4bf38 100644 --- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs +++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs @@ -10,6 +10,8 @@ using AIStudio.Settings.DataModel; using Microsoft.AspNetCore.Components; +using SharedTools; + using DialogOptions = AIStudio.Dialogs.DialogOptions; namespace AIStudio.Assistants.DocumentAnalysis; @@ -747,16 +749,12 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore> AvailableLLMProviders = new(); protected readonly List> AvailableEmbeddingProviders = new(); diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor index 060dc0ee..2f8600a8 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor @@ -43,6 +43,28 @@ + @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + { + @if (context.FileAttachments.Count == 0) + { + + + + } + else + { + + + + @T("Use shared attachment paths") + + + @T("Copy attachments into plugin") + + + + } + } diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs index 579fff22..89473518 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs @@ -98,4 +98,66 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } + + private async Task ExportChatTemplateWithSharedAttachmentPaths(ChatTemplate chatTemplate) + { + if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + return; + + if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) + return; + + await this.CopyChatTemplateLuaToClipboard(chatTemplate); + } + + private async Task ExportChatTemplateWithPackagedAttachments(ChatTemplate chatTemplate) + { + if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + return; + + if (chatTemplate == ChatTemplate.NO_CHAT_TEMPLATE || chatTemplate.IsEnterpriseConfiguration) + return; + + if (chatTemplate.FileAttachments.Count == 0) + { + await this.ExportChatTemplateWithSharedAttachmentPaths(chatTemplate); + return; + } + + var pluginDirectoryResponse = await this.RustService.SelectDirectory(T("Select configuration plugin folder")); + if (pluginDirectoryResponse.UserCancelled) + return; + + await this.CopyPackagedChatTemplateLuaToClipboard(chatTemplate, pluginDirectoryResponse.SelectedDirectory); + } + + private async Task CopyChatTemplateLuaToClipboard(ChatTemplate chatTemplate) + { + if (!chatTemplate.TryExportAsConfigurationSection(out var luaCode, out var issue)) + { + await this.DialogService.ShowMessageBox( + T("Export Chat Template"), + issue, + T("Close")); + return; + } + + if (!string.IsNullOrWhiteSpace(luaCode)) + await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode); + } + + private async Task CopyPackagedChatTemplateLuaToClipboard(ChatTemplate chatTemplate, string pluginDirectory) + { + if (!chatTemplate.TryExportAsConfigurationSectionWithPackagedAttachments(pluginDirectory, out var luaCode, out var issue)) + { + await this.DialogService.ShowMessageBox( + T("Export Chat Template"), + issue, + T("Close")); + return; + } + + if (!string.IsNullOrWhiteSpace(luaCode)) + await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs index ff706363..1f13fe54 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs @@ -3,15 +3,10 @@ using AIStudio.Settings.DataModel; using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.PluginSystem; -using Microsoft.AspNetCore.Components; - namespace AIStudio.Dialogs.Settings; public partial class SettingsDialogDataSources : SettingsDialogBase { - [Inject] - private ISnackbar Snackbar { get; init; } = null!; - private string GetEmbeddingName(IDataSource dataSource) { if(dataSource is IInternalDataSource internalDataSource) diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor index c251673b..784bfffc 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor @@ -42,6 +42,12 @@ + @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + { + + + + } diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs index 6547257c..4fb6c67a 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs @@ -49,6 +49,19 @@ public partial class SettingsDialogProfiles : SettingsDialogBase await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED); } + private async Task ExportProfile(Profile profile) + { + if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + return; + + if (profile == Profile.NO_PROFILE || profile.IsEnterpriseConfiguration) + return; + + var luaCode = profile.ExportAsConfigurationSection(); + if (!string.IsNullOrWhiteSpace(luaCode)) + await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode); + } + private async Task DeleteProfile(Profile profile) { var dialogParameters = new DialogParameters diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index ef98a1e6..93353fda 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -298,7 +298,8 @@ CONFIG["CHAT_TEMPLATES"] = {} -- ["AllowProfileUsage"] = true, -- -- Optional: Pre-attach files that will be automatically included when using this template. -- -- These files will be loaded when the user selects this chat template. --- -- Note: File paths must be absolute paths and accessible to all users. +-- -- Note: File paths can be absolute paths that are accessible to all users, or relative paths +-- -- inside this plugin folder, for example "attachments/00000000-0000-0000-0000-000000000001/Guidelines.pdf". -- ["FileAttachments"] = { -- "G:\\Company\\Documents\\Guidelines.pdf", -- "G:\\Company\\Documents\\CompanyPolicies.docx" 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 587747ae..90a16a92 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 @@ -4752,6 +4752,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] = -- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Passen Sie ihre KI-Erfahrung mit Chat-Vorlagen an. Egal, ob Sie mit Prompt-Engineering experimentieren, einfach einen eigenen System-Prompt im normalen Chat verwenden oder einen spezialisierten Assistenten erstellen möchten – mit Chat-Vorlagen haben Sie die volle Kontrolle. Ähnlich wie in den Playgrounds gängiger KI-Anbieter können Sie eigene System-Prompts festlegen und bei unterstützenden Anbietern auch Assistenten-Prompts nutzen." +-- Copy attachments into plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Anhänge in das Plugin kopieren" + -- Delete UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Löschen" @@ -4761,6 +4764,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483 -- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Hinweis: Diese fortgeschrittene Funktion richtet sich an Nutzer, die mit den Grundlagen des Prompt Engineerings vertraut sind. Außerdem müssen Sie selbst sicherstellen, dass Ihr gewählter Anbieter die Verwendung von Assistenten-Prompts unterstützt." +-- Use shared attachment paths +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Gemeinsame Pfade für Anhänge verwenden" + -- No chat templates configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "Noch keine Chat-Vorlagen konfiguriert." @@ -4779,6 +4785,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481 -- This template is managed by your organization. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "Diesee Vorlage wird von Ihrer Organisation verwaltet." +-- Select configuration plugin folder +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Konfigurationsordner für Plugins auswählen" + -- Edit Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Chat-Vorlage bearbeiten" @@ -4788,9 +4797,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38241 -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Aktionen" +-- Export +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3898821075"] = "Exportieren" + -- Delete Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Chat-Vorlage löschen" +-- Export Chat Template +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Chat-Vorlage exportieren" + +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Konfiguration exportieren" + -- Which programming language should be preselected for added contexts? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Welche Programmiersprache soll für hinzugefügte Kontexte vorausgewählt werden?" @@ -5226,6 +5244,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659" -- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Sind Sie Projektleiter in einer Forschungseinrichtung? Dann möchten Sie vielleicht ein Profil für ihre Projektmanagement-Aktivitäten anlegen, eines für ihre wissenschaftliche Arbeit und ein weiteres Profil, wenn Sie Programmcode schreiben müssen. In diesen Profilen können Sie festhalten, wie viel Erfahrung Sie haben oder welche Methoden Sie bevorzugen oder nicht gerne verwenden. Später können Sie dann auswählen, wann und wo Sie jedes Profil nutzen möchten." +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Konfiguration exportieren" + -- Preselect the target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Zielsprache vorwählen" @@ -6555,9 +6576,27 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T39077128 -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Modell wie in whisper.cpp konfiguriert" +-- Cannot export this chat template because example message {0} is not a text message. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} keine Textnachricht ist." + +-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Diese Chat-Vorlage kann nicht exportiert werden, da die Beispielnachricht {0} eine Rolle verwendet, die von Konfigurations-Plugins nicht unterstützt wird." + +-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Bitte wählen Sie einen gültigen Konfigurations-Plug-in-Ordner aus. Der Ordner muss eine Datei „plugin.lua“ enthalten." + +-- Cannot package the chat template attachments. The issue was: {0} +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Die Anhänge der Chat-Vorlage können nicht verpackt werden. Das Problem war: {0}" + +-- Cannot package the attachment '{0}' because the file does not exist. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Der Anhang „{0}“ kann nicht gepackt werden, da die Datei nicht existiert." + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Keine Chat-Vorlage verwenden" +-- Cannot export this chat template because example message {0} is empty. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Diese Chatvorlage kann nicht exportiert werden, da die Beispielnachricht {0} leer ist." + -- Navigation never expands, but there are tooltips UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Die Navigationsleiste wird nie ausgeklappt, aber es gibt Tooltips" 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 691e965f..742e8525 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 @@ -4752,6 +4752,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T582516016"] = -- Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1172171653"] = "Customize your AI experience with chat templates. Whether you want to experiment with prompt engineering, simply use a custom system prompt in the standard chat interface, or create a specialized assistant, chat templates give you full control. Similar to common AI companies' playgrounds, you can define your own system prompts and leverage assistant prompts for providers that support them." +-- Copy attachments into plugin +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin" + -- Delete UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete" @@ -4761,6 +4764,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T15483 -- Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1909110760"] = "Note: This advanced feature is designed for users familiar with prompt engineering concepts. Furthermore, you have to make sure yourself that your chosen provider supports the use of assistant prompts." +-- Use shared attachment paths +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2054531878"] = "Use shared attachment paths" + -- No chat templates configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2319860307"] = "No chat templates configured yet." @@ -4779,6 +4785,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T34481 -- This template is managed by your organization. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576775249"] = "This template is managed by your organization." +-- Select configuration plugin folder +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3576816894"] = "Select configuration plugin folder" + -- Edit Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3596030597"] = "Edit Chat Template" @@ -4788,9 +4797,18 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38241 -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3865031940"] = "Actions" +-- Export +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T3898821075"] = "Export" + -- Delete Chat Template UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T4025180906"] = "Delete Chat Template" +-- Export Chat Template +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T491504763"] = "Export Chat Template" + +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T975426229"] = "Export configuration" + -- Which programming language should be preselected for added contexts? UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1073540083"] = "Which programming language should be preselected for added contexts?" @@ -5226,6 +5244,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T55364659" -- Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T56359901"] = "Are you a project manager in a research facility? You might want to create a profile for your project management activities, one for your scientific work, and a profile for when you need to write program code. In these profiles, you can record how much experience you have or which methods you like or dislike using. Later, you can choose when and where you want to use each profile." +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T975426229"] = "Export configuration" + -- Preselect the target language UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROMPTOPTIMIZER::T1417990312"] = "Preselect the target language" @@ -6555,9 +6576,27 @@ UI_TEXT_CONTENT["AISTUDIO::PROVIDER::MODELLOADFAILUREREASONEXTENSIONS::T39077128 -- Model as configured by whisper.cpp UI_TEXT_CONTENT["AISTUDIO::PROVIDER::SELFHOSTED::PROVIDERSELFHOSTED::T3313940770"] = "Model as configured by whisper.cpp" +-- Cannot export this chat template because example message {0} is not a text message. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T1861800849"] = "Cannot export this chat template because example message {0} is not a text message." + +-- Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2407395493"] = "Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins." + +-- Please select a valid configuration plugin folder. The folder must contain a plugin.lua file. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T2542895569"] = "Please select a valid configuration plugin folder. The folder must contain a plugin.lua file." + +-- Cannot package the chat template attachments. The issue was: {0} +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T3635593138"] = "Cannot package the chat template attachments. The issue was: {0}" + +-- Cannot package the attachment '{0}' because the file does not exist. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4121340492"] = "Cannot package the attachment '{0}' because the file does not exist." + -- Use no chat template UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T4258819635"] = "Use no chat template" +-- Cannot export this chat template because example message {0} is empty. +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CHATTEMPLATE::T477540958"] = "Cannot export this chat template because example message {0} is empty." + -- Navigation never expands, but there are tooltips UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T1095779033"] = "Navigation never expands, but there are tooltips" diff --git a/app/MindWork AI Studio/Settings/ChatTemplate.cs b/app/MindWork AI Studio/Settings/ChatTemplate.cs index 78879a62..c3d93ad9 100644 --- a/app/MindWork AI Studio/Settings/ChatTemplate.cs +++ b/app/MindWork AI Studio/Settings/ChatTemplate.cs @@ -1,7 +1,11 @@ +using System.Text; + using AIStudio.Chat; using AIStudio.Tools.PluginSystem; -using Lua; +using SharedTools; + +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings; @@ -17,6 +21,8 @@ public record ChatTemplate( bool IsEnterpriseConfiguration = false, Guid EnterpriseConfigurationPluginId = default) : ConfigurationBaseObject { + private const string ATTACHMENTS_DIRECTORY = "attachments"; + public ChatTemplate() : this(0, Guid.Empty.ToString(), string.Empty, string.Empty, string.Empty, [], [], false) { } @@ -73,8 +79,8 @@ public record ChatTemplate( return this.SystemPrompt; } - - public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template) + + public static bool TryParseChatTemplateTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject template) { template = NO_CHAT_TEMPLATE; if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) @@ -103,7 +109,7 @@ public record ChatTemplate( if (table.TryGetValue("AllowProfileUsage", out var allowProfileValue) && allowProfileValue.TryRead(out var allow)) allowProfileUsage = allow; - var fileAttachments = ParseFileAttachments(idx, table); + var fileAttachments = ParseFileAttachments(idx, table, pluginPath); template = new ChatTemplate { @@ -169,7 +175,7 @@ public record ChatTemplate( return exampleConversation; } - private static List ParseFileAttachments(int idx, LuaTable table) + private static List ParseFileAttachments(int idx, LuaTable table, string pluginPath) { var fileAttachments = new List(); if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead(out var fileAttTable)) @@ -185,9 +191,227 @@ public record ChatTemplate( continue; } - fileAttachments.Add(FileAttachment.FromPath(filePath)); + if (TryResolveFileAttachmentPath(idx, attachmentNum, filePath, pluginPath, out var resolvedFilePath)) + fileAttachments.Add(FileAttachment.FromPath(resolvedFilePath)); } return fileAttachments; } + + private static bool TryResolveFileAttachmentPath(int idx, int attachmentNum, string filePath, string pluginPath, out string resolvedFilePath) + { + resolvedFilePath = filePath; + if (string.IsNullOrWhiteSpace(filePath)) + { + LOGGER.LogWarning("The FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} is empty.", attachmentNum, idx); + return false; + } + + if (Path.IsPathFullyQualified(filePath)) + return true; + + if (string.IsNullOrWhiteSpace(pluginPath)) + { + LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} cannot be resolved because the plugin path is unknown.", attachmentNum, idx); + return false; + } + + var pluginRoot = Path.GetFullPath(pluginPath); + var relativePath = filePath + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + + if (relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == "..")) + { + LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} contains '..' path segments and will be ignored.", attachmentNum, idx); + return false; + } + + var combinedPath = Path.GetFullPath(Path.Combine(pluginRoot, relativePath)); + var pluginRootWithSeparator = pluginRoot.EndsWith(Path.DirectorySeparatorChar) + ? pluginRoot + : pluginRoot + Path.DirectorySeparatorChar; + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + if (!combinedPath.StartsWith(pluginRootWithSeparator, comparison)) + { + LOGGER.LogWarning("The relative FileAttachments entry {AttachmentNum} in chat template {IdxChatTemplate} points outside of the plugin folder and will be ignored.", attachmentNum, idx); + return false; + } + + resolvedFilePath = combinedPath; + return true; + } + + public bool TryExportAsConfigurationSection(out string luaCode, out string issue) => this.TryExportAsConfigurationSection(null, Guid.NewGuid().ToString(), out luaCode, out issue); + + private bool TryExportAsConfigurationSection(IReadOnlyList? fileAttachmentPaths, string exportId, out string luaCode, out string issue) + { + luaCode = string.Empty; + issue = string.Empty; + if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue)) + return false; + + return this.TryExportAsConfigurationSection(fileAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue); + } + + private bool TryExportAsConfigurationSection(IReadOnlyList? fileAttachmentPaths, string exportId, string exampleConversationLua, out string luaCode, out string issue) + { + issue = string.Empty; + var fileAttachmentsLua = this.BuildFileAttachmentsLua(fileAttachmentPaths); + luaCode = $$""" + CONFIG["CHAT_TEMPLATES"][#CONFIG["CHAT_TEMPLATES"]+1] = { + ["Id"] = "{{LuaTools.EscapeLuaString(exportId)}}", + ["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}}, + ["SystemPrompt"] = {{LuaTools.ToLuaStringLiteral(this.SystemPrompt)}}, + ["PredefinedUserPrompt"] = {{LuaTools.ToLuaStringLiteral(this.PredefinedUserPrompt)}}, + ["AllowProfileUsage"] = {{this.AllowProfileUsage.ToString().ToLowerInvariant()}}, + ["FileAttachments"] = {{fileAttachmentsLua}}, + ["ExampleConversation"] = {{exampleConversationLua}}, + } + """; + return true; + } + + public bool TryExportAsConfigurationSectionWithPackagedAttachments(string pluginDirectory, out string luaCode, out string issue) + { + luaCode = string.Empty; + issue = string.Empty; + var exportId = Guid.NewGuid().ToString(); + + if (!this.TryBuildExampleConversationLua(out var exampleConversationLua, out issue)) + return false; + + if (this.FileAttachments.Count == 0) + return this.TryExportAsConfigurationSection(null, exportId, exampleConversationLua, out luaCode, out issue); + + if (string.IsNullOrWhiteSpace(pluginDirectory) || !File.Exists(Path.Combine(pluginDirectory, "plugin.lua"))) + { + issue = TB("Please select a valid configuration plugin folder. The folder must contain a plugin.lua file."); + return false; + } + + var sourcePaths = new List(); + foreach (var attachment in this.FileAttachments) + { + if (string.IsNullOrWhiteSpace(attachment.FilePath) || !File.Exists(attachment.FilePath)) + { + issue = string.Format(TB("Cannot package the attachment '{0}' because the file does not exist."), attachment.FileName); + return false; + } + + sourcePaths.Add(attachment.FilePath); + } + + var targetDirectory = Path.Combine(pluginDirectory, ATTACHMENTS_DIRECTORY, exportId); + var relativeAttachmentPaths = new List(); + var usedFileNames = new HashSet(StringComparer.OrdinalIgnoreCase); + try + { + Directory.CreateDirectory(targetDirectory); + foreach (var sourcePath in sourcePaths) + { + var targetFileName = CreateUniqueAttachmentFileName(sourcePath, usedFileNames); + var targetPath = Path.Combine(targetDirectory, targetFileName); + File.Copy(sourcePath, targetPath, overwrite: false); + relativeAttachmentPaths.Add($"{ATTACHMENTS_DIRECTORY}/{exportId}/{targetFileName}"); + } + } + catch (Exception e) + { + try + { + if (Directory.Exists(targetDirectory)) + Directory.Delete(targetDirectory, true); + } + catch + { + // Keep the original packaging error as the user-facing issue. + } + + issue = string.Format(TB("Cannot package the chat template attachments. The issue was: {0}"), e.Message); + return false; + } + + return this.TryExportAsConfigurationSection(relativeAttachmentPaths, exportId, exampleConversationLua, out luaCode, out issue); + } + + private bool TryBuildExampleConversationLua(out string luaTable, out string issue) + { + luaTable = "{}"; + issue = string.Empty; + if (this.ExampleConversation.Count == 0) + return true; + + var builder = new StringBuilder(); + builder.AppendLine("{"); + for (var i = 0; i < this.ExampleConversation.Count; i++) + { + var block = this.ExampleConversation[i]; + if (block.Role is not ChatRole.USER and not ChatRole.AI) + { + issue = string.Format(TB("Cannot export this chat template because example message {0} uses a role that is not supported by configuration plugins."), i + 1); + return false; + } + + if (block.Content is not ContentText textContent) + { + issue = string.Format(TB("Cannot export this chat template because example message {0} is not a text message."), i + 1); + return false; + } + + if (string.IsNullOrWhiteSpace(textContent.Text)) + { + issue = string.Format(TB("Cannot export this chat template because example message {0} is empty."), i + 1); + return false; + } + + builder.AppendLine(" {"); + builder.AppendLine($" [\"Role\"] = \"{block.Role}\","); + builder.AppendLine($" [\"Content\"] = {LuaTools.ToLuaStringLiteral(textContent.Text)},"); + builder.AppendLine(" },"); + } + + builder.Append(" }"); + luaTable = builder.ToString(); + return true; + } + + private string BuildFileAttachmentsLua(IReadOnlyList? fileAttachmentPaths) + { + var paths = fileAttachmentPaths ?? this.FileAttachments.Select(attachment => attachment.FilePath).ToList(); + if (paths.Count == 0) + return "{}"; + + var builder = new StringBuilder(); + builder.AppendLine("{"); + foreach (var path in paths) + builder.AppendLine($" \"{LuaTools.EscapeLuaString(path)}\","); + + builder.Append(" }"); + return builder.ToString(); + } + + private static string CreateUniqueAttachmentFileName(string sourcePath, HashSet usedFileNames) + { + var fileName = SanitizeFileName(Path.GetFileName(sourcePath)); + if (string.IsNullOrWhiteSpace(fileName)) + fileName = "attachment"; + + var extension = Path.GetExtension(fileName); + var nameWithoutExtension = Path.GetFileNameWithoutExtension(fileName); + var candidate = fileName; + var counter = 2; + while (!usedFileNames.Add(candidate)) + candidate = $"{nameWithoutExtension}-{counter++}{extension}"; + + return candidate; + } + + private static string SanitizeFileName(string fileName) + { + foreach (var invalidChar in Path.GetInvalidFileNameChars()) + fileName = fileName.Replace(invalidChar, '_'); + + return fileName; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs index cd254751..3fb7bd1a 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs @@ -8,10 +8,11 @@ using AIStudio.Tools.PluginSystem; using AIStudio.Tools.RAG; using AIStudio.Tools.Services; -using Lua; +using SharedTools; using ChatThread = AIStudio.Chat.ChatThread; using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType; +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings.DataModel; diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index d5a6f20a..576defe2 100644 --- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs +++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs @@ -3,9 +3,10 @@ using System.Text.Json.Serialization; using AIStudio.Provider; using AIStudio.Tools.PluginSystem; -using Lua; +using SharedTools; using Host = AIStudio.Provider.SelfHosted.Host; +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings; diff --git a/app/MindWork AI Studio/Settings/Profile.cs b/app/MindWork AI Studio/Settings/Profile.cs index 2129b04c..925a5936 100644 --- a/app/MindWork AI Studio/Settings/Profile.cs +++ b/app/MindWork AI Studio/Settings/Profile.cs @@ -1,5 +1,8 @@ using AIStudio.Tools.PluginSystem; -using Lua; + +using SharedTools; + +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings; @@ -132,4 +135,20 @@ public record Profile( return true; } + + /// + /// Exports the profile configuration as a Lua configuration section. + /// + /// A Lua configuration section string. + public string ExportAsConfigurationSection() + { + return $$""" + CONFIG["PROFILES"][#CONFIG["PROFILES"]+1] = { + ["Id"] = "{{Guid.NewGuid().ToString()}}", + ["Name"] = {{LuaTools.ToLuaStringLiteral(this.Name)}}, + ["NeedToKnow"] = {{LuaTools.ToLuaStringLiteral(this.NeedToKnow)}}, + ["Actions"] = {{LuaTools.ToLuaStringLiteral(this.Actions)}}, + } + """; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index 0ccf272c..88adde29 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -4,9 +4,10 @@ using AIStudio.Provider; using AIStudio.Provider.HuggingFace; using AIStudio.Tools.PluginSystem; -using Lua; +using SharedTools; using Host = AIStudio.Provider.SelfHosted.Host; +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings; diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index 4c6ca871..ca95d821 100644 --- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs +++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs @@ -3,9 +3,10 @@ using System.Text.Json.Serialization; using AIStudio.Provider; using AIStudio.Tools.PluginSystem; -using Lua; +using SharedTools; using Host = AIStudio.Provider.SelfHosted.Host; +using LuaTable = Lua.LuaTable; namespace AIStudio.Settings; diff --git a/app/MindWork AI Studio/Tools/LuaTools.cs b/app/MindWork AI Studio/Tools/LuaTools.cs deleted file mode 100644 index 0df50cd0..00000000 --- a/app/MindWork AI Studio/Tools/LuaTools.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace AIStudio.Tools; - -public static class LuaTools -{ - public static string EscapeLuaString(string? value) - { - if (string.IsNullOrEmpty(value)) - return string.Empty; - - return value - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\r", "\\r") - .Replace("\n", "\\n"); - } -} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index dd422c06..29548eca 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -185,7 +185,7 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun); // Handle configured chat templates: - PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun); + PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath); // Handle configured data sources: PluginConfigurationObject.TryParseDataSources(mainTable, this.Id, ref this.configObjects, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index 26f10e7d..934de5dc 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -52,6 +52,7 @@ public sealed record PluginConfigurationObject /// This parameter is passed by reference. /// Specifies whether to perform the operation as a dry run, where changes /// are not persisted. + /// An optional parameter specifying the file path of the plugin, used for relative paths in the Lua table. /// Returns true if parsing succeeds and configuration objects are added /// to the list; otherwise, false. public static bool TryParse( @@ -61,7 +62,8 @@ public sealed record PluginConfigurationObject LuaTable mainTable, Guid configPluginId, ref List configObjects, - bool dryRun + bool dryRun, + string pluginPath = "" ) where TClass : ConfigurationBaseObject { var luaTableName = configObjectType switch @@ -104,7 +106,7 @@ public sealed record PluginConfigurationObject var (wasParsingSuccessful, configObject) = configObjectType switch { PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject), - PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject), + PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject), PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject), PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject), PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject), diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index b0dfd89d..c939899d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -326,7 +326,11 @@ public static partial class PluginFactory return new PluginLanguage(isInternal, state, type); case PluginType.CONFIGURATION: - var configPlug = new PluginConfiguration(isInternal, state, type); + var configPlug = new PluginConfiguration(isInternal, state, type) + { + PluginPath = pluginPath ?? string.Empty + }; + await configPlug.InitializeAsync(true); return configPlug; diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index 4a066843..a9c0b337 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -6,9 +6,11 @@ public sealed partial class RustService { public async Task SelectDirectory(string title, string? initialDirectory = null) { - PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory); var encodedTitle = Uri.EscapeDataString(title); - var result = await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", previousDirectory, this.jsonRustSerializerOptions); + var result = initialDirectory is null + ? await this.http.PostAsync($"/select/directory?title={encodedTitle}", null) + : await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", new PreviousDirectory(initialDirectory), this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) { this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'"); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 67c080e9..2620bda9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1,5 +1,6 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) - Released the voice recording and transcription for all users. You no longer need to enable a preview feature to configure transcription providers, select a transcription provider, or use dictation. +- Added export options for profiles and chat templates, including an option to package chat template attachments into configuration plugins. - Added support for organization-managed ERI servers in configuration plugins, so admins can preconfigure external data sources for users. - Added an export option for ERI server data sources, so admins can create configuration plugin snippets without writing the Lua code manually. - Added an option to configure the timeout setting for all requests. This is useful when you have a slow network connection, or you have to work with slow AI servers. It is also possible to configure this timeout for an entire organization using configuration plugins. diff --git a/app/SharedTools/LuaTools.cs b/app/SharedTools/LuaTools.cs index 53bd07c6..10eea7ac 100644 --- a/app/SharedTools/LuaTools.cs +++ b/app/SharedTools/LuaTools.cs @@ -2,9 +2,49 @@ namespace SharedTools; public static class LuaTools { - public static string EscapeLuaString(string value) + public static string EscapeLuaString(string? value) { + if (string.IsNullOrEmpty(value)) + return string.Empty; + // Replace backslashes with double backslashes and escape double quotes: - return value.Replace("\\", @"\\").Replace("\"", "\\\""); + return value + .Replace("\\", @"\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n"); + } + + public static string ToLuaStringLiteral(string? value, bool forceLongString = false, int longStringLengthThreshold = 80) + { + value ??= string.Empty; + if (!forceLongString && + value.Length <= longStringLengthThreshold && + !value.Contains('\n') && + !value.Contains('\r')) + return $"\"{EscapeLuaString(value)}\""; + + return $"{CreateLongStringOpeningDelimiter(value)}{value}{CreateLongStringClosingDelimiter(value)}"; + } + + private static string CreateLongStringOpeningDelimiter(string value) + { + var equals = CreateLongStringEquals(value); + return $"[{equals}["; + } + + private static string CreateLongStringClosingDelimiter(string value) + { + var equals = CreateLongStringEquals(value); + return $"]{equals}]"; + } + + private static string CreateLongStringEquals(string value) + { + var equalsCount = 3; + while (value.Contains($"]{new string('=', equalsCount)}]")) + equalsCount++; + + return new string('=', equalsCount); } } \ No newline at end of file