Added support for exporting chat templates & profiles (#772)
Some checks are pending
Build and Release / Determine run mode (push) Waiting to run
Build and Release / Read metadata (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-apple-darwin, osx-arm64, macos-latest, aarch64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-pc-windows-msvc.exe, win-arm64, windows-latest, aarch64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-aarch64-unknown-linux-gnu, linux-arm64, ubuntu-22.04-arm, aarch64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-apple-darwin, osx-x64, macos-latest, x86_64-apple-darwin, dmg,app,updater, dmg) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-pc-windows-msvc.exe, win-x64, windows-latest, x86_64-pc-windows-msvc, nsis,updater, nsis) (push) Blocked by required conditions
Build and Release / Build app (${{ matrix.dotnet_runtime }}) (-x86_64-unknown-linux-gnu, linux-x64, ubuntu-22.04, x86_64-unknown-linux-gnu, appimage,updater, appimage) (push) Blocked by required conditions
Build and Release / Prepare & create release (push) Blocked by required conditions
Build and Release / Publish release (push) Blocked by required conditions

This commit is contained in:
Thorsten Sommer 2026-05-22 15:46:03 +02:00 committed by GitHub
parent 277309cd19
commit c08f9e2ea1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 543 additions and 49 deletions

View File

@ -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<NoSettingsPan
return $$"""
CONFIG["DOCUMENT_ANALYSIS_POLICIES"][#CONFIG["DOCUMENT_ANALYSIS_POLICIES"]+1] = {
["Id"] = "{{id}}",
["PolicyName"] = "{{this.selectedPolicy.PolicyName.Trim()}}",
["PolicyDescription"] = "{{this.selectedPolicy.PolicyDescription.Trim()}}",
["PolicyName"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyName.Trim())}},
["PolicyDescription"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.PolicyDescription.Trim())}},
["AnalysisRules"] = [===[
{{this.selectedPolicy.AnalysisRules.Trim()}}
]===],
["AnalysisRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.AnalysisRules.Trim(), forceLongString: true)}},
["OutputRules"] = [===[
{{this.selectedPolicy.OutputRules.Trim()}}
]===],
["OutputRules"] = {{LuaTools.ToLuaStringLiteral(this.selectedPolicy.OutputRules.Trim(), forceLongString: true)}},
-- Optional: minimum provider confidence required for this policy.
-- Allowed values are: NONE, VERY_LOW, LOW, MODERATE, MEDIUM, HIGH

View File

@ -4750,6 +4750,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, our 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, our 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"
@ -4759,6 +4762,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."
@ -4777,6 +4783,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"
@ -4789,6 +4798,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T38650
-- 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?"
@ -5224,6 +5239,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"
@ -6553,9 +6571,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"

View File

@ -19,6 +19,9 @@ public abstract class SettingsDialogBase : MSGComponentBase
[Inject]
protected RustService RustService { get; init; } = null!;
[Inject]
protected ISnackbar Snackbar { get; init; } = null!;
protected readonly List<ConfigurationSelectData<string>> AvailableLLMProviders = new();
protected readonly List<ConfigurationSelectData<string>> AvailableEmbeddingProviders = new();

View File

@ -43,6 +43,28 @@
<MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="@(() => this.EditChatTemplate(context))"/>
</MudTooltip>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
{
@if (context.FileAttachments.Count == 0)
{
<MudTooltip Text="@T("Export configuration")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))"/>
</MudTooltip>
}
else
{
<MudTooltip Text="@T("Export configuration")">
<MudMenu Icon="@Icons.Material.Filled.Dataset" Color="Color.Info" Variant="Variant.Text">
<MudMenuItem Icon="@Icons.Material.Filled.Link" OnClick="@(() => this.ExportChatTemplateWithSharedAttachmentPaths(context))">
@T("Use shared attachment paths")
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Folder" OnClick="@(() => this.ExportChatTemplateWithPackagedAttachments(context))">
@T("Copy attachments into plugin")
</MudMenuItem>
</MudMenu>
</MudTooltip>
}
}
<MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="@(() => this.DeleteChatTemplate(context))"/>
</MudTooltip>

View File

@ -98,4 +98,66 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
await this.MessageBus.SendMessage<bool>(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);
}
}

View File

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

View File

@ -42,6 +42,12 @@
<MudTooltip Text="@T("Edit")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Edit" OnClick="() => this.EditProfile(context)"/>
</MudTooltip>
@if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings)
{
<MudTooltip Text="@T("Export configuration")">
<MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportProfile(context)"/>
</MudTooltip>
}
<MudTooltip Text="@T("Delete")">
<MudIconButton Color="Color.Error" Icon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteProfile(context)"/>
</MudTooltip>

View File

@ -49,6 +49,19 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
await this.MessageBus.SendMessage<bool>(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<ConfirmDialog>

View File

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

View File

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

View File

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

View File

@ -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)
{
}
@ -74,7 +80,7 @@ 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<string>(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<bool>(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<FileAttachment> ParseFileAttachments(int idx, LuaTable table)
private static List<FileAttachment> ParseFileAttachments(int idx, LuaTable table, string pluginPath)
{
var fileAttachments = new List<FileAttachment>();
if (!table.TryGetValue("FileAttachments", out var fileAttValue) || !fileAttValue.TryRead<LuaTable>(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<string>? 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<string>? 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<string>();
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<string>();
var usedFileNames = new HashSet<string>(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<string>? 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<string> 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;
}
}

View File

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

View File

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

View File

@ -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;
}
/// <summary>
/// Exports the profile configuration as a Lua configuration section.
/// </summary>
/// <returns>A Lua configuration section string.</returns>
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)}},
}
""";
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -52,6 +52,7 @@ public sealed record PluginConfigurationObject
/// This parameter is passed by reference.</param>
/// <param name="dryRun">Specifies whether to perform the operation as a dry run, where changes
/// are not persisted.</param>
/// <param name="pluginPath">An optional parameter specifying the file path of the plugin, used for relative paths in the Lua table.</param>
/// <returns>Returns true if parsing succeeds and configuration objects are added
/// to the list; otherwise, false.</returns>
public static bool TryParse<TClass>(
@ -61,7 +62,8 @@ public sealed record PluginConfigurationObject
LuaTable mainTable,
Guid configPluginId,
ref List<PluginConfigurationObject> 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),

View File

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

View File

@ -6,9 +6,11 @@ public sealed partial class RustService
{
public async Task<DirectorySelectionResponse> 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}'");

View File

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

View File

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