diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
index b43a5cb7..9eb13f21 100644
--- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
+++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor
@@ -47,6 +47,7 @@ else
@T("Add policy")
+
@T("Delete this policy")
diff --git a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs
index e69dbc12..3e31c052 100644
--- a/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs
+++ b/app/MindWork AI Studio/Assistants/DocumentAnalysis/DocumentAnalysisAssistant.razor.cs
@@ -9,6 +9,7 @@ using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components;
+using LuaTable = Lua.LuaTable;
using SharedTools;
@@ -244,7 +245,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore this.AddPolicy(null);
+
+ private async Task ImportPolicy()
+ {
+ if (this.ArePolicyControlsDisabled || !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "DOCUMENT_ANALYSIS_POLICIES", T("Import document analysis policy"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
+ await this.AddPolicy(table);
+ }
+
+ private async Task AddPolicy(LuaTable? importedConfiguration)
+ {
+ if (this.ArePolicyControlsDisabled)
+ return;
+
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
+ return;
+
+ var parameters = new DialogParameters();
+ if (importedConfiguration is not null)
+ parameters.Add(x => x.ImportedConfiguration, importedConfiguration);
+ var dialogReference = await this.DialogService.ShowAsync(T("Add policy"), parameters, DialogOptions.FULLSCREEN);
+ var result = await dialogReference.Result;
+ if (result is null || result.Canceled || result.Data is not DataDocumentAnalysisPolicy policy ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES")))
+ return;
+
+ var addedPolicy = policy with
+ {
+ Num = this.SettingsManager.ConfigurationData.NextDocumentAnalysisPolicyNum++,
+ };
+ this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.Add(addedPolicy);
+ await this.SettingsManager.StoreSettings();
+ this.SelectedPolicyChanged(addedPolicy);
+ }
+
+ private async Task AddInitialPolicy()
{
if (this.ArePolicyControlsDisabled)
return;
diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
index a36ed2f4..c69fc6c5 100644
--- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
+++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua
@@ -1102,6 +1102,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA
-- Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T1291179736"] = "Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents."
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T1463683828"] = "Import"
+
-- Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T1692505801"] = "Only the tools selected here can be used by the AI for an analysis with this policy. Every tool still has to meet the confidence requirements of the selected provider, so a tool may remain unavailable even when this policy permits it."
@@ -1216,6 +1219,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA
-- A policy with this name already exists. Please choose a different name.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T3584374593"] = "A policy with this name already exists. Please choose a different name."
+-- Import document analysis policy
+UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T3800115485"] = "Import document analysis policy"
+
-- Load analysis rules from document
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T3813558135"] = "Load analysis rules from document"
@@ -3919,6 +3925,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T750361472"] = "Can
-- External Data (ERI-Server v1)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T774473996"] = "External Data (ERI-Server v1)"
+-- Import ERI v1 Data Source
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T777621786"] = "Import ERI v1 Data Source"
+
-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCEMANAGEMENT::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"
@@ -4846,6 +4855,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELDATASOURCES::T4761
-- Embedding Result
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1387042335"] = "Embedding Result"
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1463683828"] = "Import"
+
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T1469573738"] = "Delete"
@@ -4879,6 +4891,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T21748
-- Model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2189814010"] = "Model"
+-- Import Embedding Provider
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T229678442"] = "Import Embedding Provider"
+
-- Embeddings are a way to represent words, sentences, entire documents, or even images and videos as digital fingerprints. Just like each person has a unique fingerprint, embedding models create unique digital patterns that capture the meaning and characteristics of the content they analyze. When two things are similar in meaning or content, their digital fingerprints will look very similar. For example, the fingerprints for 'happy' and 'joyful' would be more alike than those for 'happy' and 'sad'.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELEMBEDDINGS::T2419962612"] = "Embeddings are a way to represent words, sentences, entire documents, or even images and videos as digital fingerprints. Just like each person has a unique fingerprint, embedding models create unique digital patterns that capture the meaning and characteristics of the content they analyze. When two things are similar in meaning or content, their digital fingerprints will look very similar. For example, the fingerprints for 'happy' and 'joyful' would be more alike than those for 'happy' and 'sad'."
@@ -4948,12 +4963,18 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERBASE::T401
-- This provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1298650849"] = "This provider is trusted by your organization for data source security checks."
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1463683828"] = "Import"
+
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1469573738"] = "Delete"
-- Uses the provider-configured model
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1760715963"] = "Uses the provider-configured model"
+-- Import LLM Provider
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T178720520"] = "Import LLM Provider"
+
-- Add Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T1806589097"] = "Add Provider"
@@ -5050,6 +5071,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T10
-- Edit Transcription Provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1317362918"] = "Edit Transcription Provider"
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1463683828"] = "Import"
+
-- Delete
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T1469573738"] = "Delete"
@@ -5092,6 +5116,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T58
-- This transcription provider is trusted by your organization for data source security checks.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T601264181"] = "This transcription provider is trusted by your organization for data source security checks."
+-- Import Transcription Provider
+UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T682006405"] = "Import Transcription Provider"
+
-- This transcription provider is managed by your organization. You can set your own API key.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELTRANSCRIPTION::T690752279"] = "This transcription provider is managed by your organization. You can set your own API key."
@@ -5722,6 +5749,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1396308587"] = "The cha
-- The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1442266827"] = "The same goes for your data. A chat template may bring its own data source options, which includes leaving the choice of sources to the AI. Without them, those chats start with the data source options from your chat options."
+-- Enter an absolute path to an existing local file before saving.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1482137067"] = "Enter an absolute path to an existing local file before saving."
+
-- Please enter a name for the chat template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T1548747185"] = "Please enter a name for the chat template."
@@ -5740,6 +5770,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T204496403"] = "The chat
-- Profile Usage
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2147062613"] = "Profile Usage"
+-- Relink attachment: {0}
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2234793579"] = "Relink attachment: {0}"
+
-- Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2292424657"] = "Add messages of an example conversation (user prompt followed by assistant prompt) to demonstrate the desired interaction pattern. These examples help the AI understand your expectations by showing it the correct format, style, and content of responses before it receives actual user inputs."
@@ -5800,6 +5833,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3127437308"] = "Are you
-- Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3227981830"] = "Using some chat templates in tandem with profiles might cause issues. Therefore, you might prohibit the usage of profiles here."
+-- Relink the missing attachment '{0}' to an existing local file before saving.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3262119677"] = "Relink the missing attachment '{0}' to an existing local file before saving."
+
-- No, chats keep the data source options from your chat options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, chats keep the data source options from your chat options"
@@ -5953,12 +5989,30 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"
-- {0} embedding provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} embedding provider"
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONSNIPPETIMPORTDIALOG::T1463683828"] = "Import"
+
+-- Configuration snippet
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONSNIPPETIMPORTDIALOG::T3867536704"] = "Configuration snippet"
+
+-- Import is locked by your organization.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONSNIPPETIMPORTDIALOG::T4101825749"] = "Import is locked by your organization."
+
+-- Copy one exported configuration snippet from the item's Export configuration control and paste it below. You can review and change the filled form before saving a new local item.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONSNIPPETIMPORTDIALOG::T4214407984"] = "Copy one exported configuration snippet from the item's Export configuration control and paste it below. You can review and change the filled form before saving a new local item."
+
+-- Cancel
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONSNIPPETIMPORTDIALOG::T900713019"] = "Cancel"
+
-- No
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T1642511898"] = "No"
-- Yes
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIRMDIALOG::T3013883440"] = "Yes"
+-- The imported retrieval process '{0}' is unavailable. Select another process before saving.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1DIALOG::T1759490982"] = "The imported retrieval process '{0}' is unavailable. Select another process before saving."
+
-- How many matches do you want at most per query?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1DIALOG::T1827669611"] = "How many matches do you want at most per query?"
@@ -6532,6 +6586,72 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T854110894
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T900713019"] = "Cancel"
+-- Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1291179736"] = "Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents."
+
+-- Hide the policy definition when distributed via configuration plugin?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1875622568"] = "Hide the policy definition when distributed via configuration plugin?"
+
+-- No profile
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2028602035"] = "No profile"
+
+-- Load output rules from document
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2168201568"] = "Load output rules from document"
+
+-- Preselect a profile
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2322771068"] = "Preselect a profile"
+
+-- The name of your policy must be between 6 and 60 characters long.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2435013256"] = "The name of your policy must be between 6 and 60 characters long."
+
+-- Preselect a provider
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2440815970"] = "Preselect a provider"
+
+-- Add
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2646845972"] = "Add"
+
+-- Policy name
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T2879019438"] = "Policy name"
+
+-- Analysis rules
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3108719748"] = "Analysis rules"
+
+-- Tools this policy permits
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T31356122"] = "Tools this policy permits"
+
+-- The description of your policy must be between 32 and 512 characters long.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3285636934"] = "The description of your policy must be between 32 and 512 characters long."
+
+-- A policy with this name already exists. Please choose a different name.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3584374593"] = "A policy with this name already exists. Please choose a different name."
+
+-- Use app default profile
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3587225583"] = "Use app default profile"
+
+-- No provider
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3740605451"] = "No provider"
+
+-- Load analysis rules from document
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3813558135"] = "Load analysis rules from document"
+
+-- Output rules
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3918193587"] = "Output rules"
+
+-- Please provide a name for your policy. This name will be used to identify the policy in AI Studio.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T4040507702"] = "Please provide a name for your policy. This name will be used to identify the policy in AI Studio."
+
+-- Please provide a description of your output rules. This rules will be used to instruct the AI on how to format the output of the analysis.
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T652187065"] = "Please provide a description of your output rules. This rules will be used to instruct the AI on how to format the output of the analysis."
+
+-- Policy description
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T748735777"] = "Policy description"
+
+-- Would you like to protect this policy so that you cannot accidentally edit or delete it?
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T80597472"] = "Would you like to protect this policy so that you cannot accidentally edit or delete it?"
+
+-- Cancel
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T900713019"] = "Cancel"
+
-- Please wait while we load the content of your file. Depending on the file type and size, this may take a moment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTCHECKDIALOG::T1205126512"] = "Please wait while we load the content of your file. Depending on the file type and size, this may take a moment."
@@ -7852,6 +7972,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T11721
-- Copy attachments into plugin
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1345613295"] = "Copy attachments into plugin"
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1463683828"] = "Import"
+
-- Delete
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T1469573738"] = "Delete"
@@ -7873,6 +7996,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T23198
-- This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T2563631533"] = "This chat template preselects data sources which exist on this machine only: {0}. They cannot be rolled out, so a chat started with this template elsewhere begins without them. Do you want to export the template anyway?"
+-- Import Chat Template
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T263213584"] = "Import Chat Template"
+
-- Chat Template Name
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T275026390"] = "Chat Template Name"
@@ -8194,9 +8320,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T711745239"
-- Edit Profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1143111468"] = "Edit Profile"
+-- Import Profile
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1215374025"] = "Import Profile"
+
-- No profiles configured yet.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1433534732"] = "No profiles configured yet."
+-- Import
+UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1463683828"] = "Import"
+
-- Delete
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGPROFILES::T1469573738"] = "Delete"
diff --git a/app/MindWork AI Studio/Components/DataSourceManagement.razor b/app/MindWork AI Studio/Components/DataSourceManagement.razor
index 7d2010a0..5402f773 100644
--- a/app/MindWork AI Studio/Components/DataSourceManagement.razor
+++ b/app/MindWork AI Studio/Components/DataSourceManagement.razor
@@ -105,14 +105,17 @@
}
-
-
- @T("External Data (ERI-Server v1)")
-
-
- @T("Local Directory")
-
-
- @T("Local File")
-
-
\ No newline at end of file
+
+
+
+ @T("External Data (ERI-Server v1)")
+
+
+ @T("Local Directory")
+
+
+ @T("Local File")
+
+
+
+
diff --git a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
index 2aaacbb7..195d32fe 100644
--- a/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
+++ b/app/MindWork AI Studio/Components/DataSourceManagement.razor.cs
@@ -6,6 +6,7 @@ using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Services;
using Microsoft.AspNetCore.Components;
+using Lua;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
@@ -170,8 +171,20 @@ public partial class DataSourceManagement : MSGComponentBase
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
- private async Task AddDataSource(DataSourceType type)
+ private async Task ImportERIDataSource()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "DATA_SOURCES", T("Import ERI v1 Data Source"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))
+ await this.AddDataSource(DataSourceType.ERI_V1, table);
+ }
+
+ private async Task AddDataSource(DataSourceType type, LuaTable? importedConfiguration = null)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))
+ return;
+
IDataSource? addedDataSource = null;
switch (type)
{
@@ -214,10 +227,13 @@ public partial class DataSourceManagement : MSGComponentBase
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ eriDialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var eriDialogReference = await this.DialogService.ShowAsync(T("Add ERI v1 Data Source"), eriDialogParameters, DialogOptions.FULLSCREEN);
var eriDialogResult = await eriDialogReference.Result;
- if (eriDialogResult is null || eriDialogResult.Canceled)
+ if (eriDialogResult is null || eriDialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES")))
return;
var eriDataSource = (DataSourceERI_V1)eriDialogResult.Data!;
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
index 31db52cb..47ca405d 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor
@@ -93,6 +93,9 @@
}
-
+
+
+
+
}
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
index a48b3f7d..4a83f8a4 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs
@@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
+using Lua;
+
namespace AIStudio.Components.Settings;
public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
@@ -53,16 +55,33 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
#endregion
- private async Task AddEmbeddingProvider()
+ private Task AddEmbeddingProvider() => this.AddEmbeddingProvider(null);
+
+ private async Task ImportEmbeddingProvider()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "EMBEDDING_PROVIDERS", T("Import Embedding Provider"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))
+ await this.AddEmbeddingProvider(table);
+ }
+
+ private async Task AddEmbeddingProvider(LuaTable? importedConfiguration)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync(T("Add Embedding Provider"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
- if (dialogResult is null || dialogResult.Canceled)
+ if (dialogResult is null || dialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS")))
return;
var addedEmbedding = (EmbeddingProvider)dialogResult.Data!;
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
index 7fd0d9da..3e016d5d 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor
@@ -78,5 +78,8 @@
}
-
+
+
+
+
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
index 64464e24..cb1873f3 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs
@@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
+using Lua;
+
namespace AIStudio.Components.Settings;
public partial class SettingsPanelProviders : SettingsPanelProviderBase
@@ -39,17 +41,34 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase
#endregion
- [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
- private async Task AddLLMProvider()
+ private Task AddLLMProvider() => this.AddLLMProvider(null);
+
+ private async Task ImportProvider()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "LLM_PROVIDERS", T("Import LLM Provider"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))
+ await this.AddLLMProvider(table);
+ }
+
+ [SuppressMessage("Usage", "MWAIS0001:Direct access to `Providers` is not allowed", Justification = "Managing the provider list is the purpose of this settings panel. Reading providers goes through the settings manager, but adding, editing, and removing them stays here on purpose.")]
+ private async Task AddLLMProvider(LuaTable? importedConfiguration)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync(T("Add LLM Provider"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
- if (dialogResult is null || dialogResult.Canceled)
+ if (dialogResult is null || dialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS")))
return;
var addedProvider = (AIStudio.Settings.Provider)dialogResult.Data!;
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
index 4ad4488f..1d864162 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor
@@ -83,6 +83,9 @@
}
-
+
+
+
+
}
diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
index 1db25379..3eff0a1d 100644
--- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
+++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs
@@ -5,6 +5,8 @@ using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
+using Lua;
+
namespace AIStudio.Components.Settings;
public partial class SettingsPanelTranscription : SettingsPanelProviderBase
@@ -47,16 +49,33 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase
#endregion
- private async Task AddTranscriptionProvider()
+ private Task AddTranscriptionProvider() => this.AddTranscriptionProvider(null);
+
+ private async Task ImportTranscriptionProvider()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "TRANSCRIPTION_PROVIDERS", T("Import Transcription Provider"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))
+ await this.AddTranscriptionProvider(table);
+ }
+
+ private async Task AddTranscriptionProvider(LuaTable? importedConfiguration)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync(T("Add Transcription Provider"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
- if (dialogResult is null || dialogResult.Canceled)
+ if (dialogResult is null || dialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS")))
return;
var addedTranscription = (TranscriptionProvider)dialogResult.Data!;
diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
index c26d4872..182dc73c 100644
--- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor
@@ -4,6 +4,25 @@
+ @if (!this.IsEditing && !this.IsReadOnly)
+ {
+ @if (!string.IsNullOrWhiteSpace(this.importReferenceIssue))
+ {
+ @this.importReferenceIssue
+ }
+ @if (!string.IsNullOrWhiteSpace(this.relinkIssue))
+ {
+ @this.relinkIssue
+ }
+ @for (var index = 0; index < this.attachmentsToRelink.Count; index++)
+ {
+ var currentIndex = index;
+
+ }
+ }
@* A drop anywhere in this dialog belongs to the dialog, not to the page behind it: *@
@* The name for the drop state is given although nobody uses it: the messages of this dialog
are listed in a table, whose rows would otherwise ask for the same name. *@
@@ -265,4 +284,4 @@
}
}
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs
index 942d2727..3b090e82 100644
--- a/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ChatTemplateDialog.razor.cs
@@ -2,6 +2,10 @@ using AIStudio.Chat;
using AIStudio.Components;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
+using AIStudio.Tools.ToolCallingSystem;
+
+using Lua;
using Microsoft.AspNetCore.Components;
@@ -9,6 +13,9 @@ namespace AIStudio.Dialogs;
public partial class ChatTemplateDialog : MSGComponentBase
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@@ -81,6 +88,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
[Inject]
private ILogger Logger { get; init; } = null!;
+ [Inject]
+ private ToolRegistry ToolRegistry { get; init; } = null!;
+
private static readonly Dictionary SPELLCHECK_ATTRIBUTES = new();
///
@@ -91,6 +101,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
private bool dataIsValid;
private List dataExampleConversation = [];
private HashSet fileAttachments = [];
+ private List<(string OriginalPath, string ReplacementPath)> attachmentsToRelink = [];
+ private string relinkIssue = string.Empty;
+ private string importReferenceIssue = string.Empty;
private bool preselectTools;
private HashSet selectedToolIds = new(StringComparer.Ordinal);
private bool preselectDataSources;
@@ -153,6 +166,15 @@ public partial class ChatTemplateDialog : MSGComponentBase
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
+
await base.OnAfterRenderAsync(firstRender);
}
@@ -176,6 +198,72 @@ public partial class ChatTemplateDialog : MSGComponentBase
IsEnterpriseConfiguration = false,
};
+ private async Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ ConfigurationImportFields.String(table, "Name");
+ ConfigurationImportFields.String(table, "SystemPrompt");
+ ConfigurationImportFields.String(table, "PredefinedUserPrompt", required: false);
+ ConfigurationImportFields.Bool(table, "AllowProfileUsage");
+ var messages = ConfigurationImportFields.Table(table, "ExampleConversation");
+ for (var index = 1; index <= messages.ArrayLength; index++)
+ {
+ if (messages[index].Type is not LuaValueType.Table || !messages[index].TryRead(out var message))
+ throw new FormatException("An example conversation entry is not a table.");
+ ConfigurationImportFields.Enum(message, "Role");
+ if (string.IsNullOrWhiteSpace(ConfigurationImportFields.String(message, "Content")))
+ throw new FormatException("An example conversation message is empty.");
+ }
+ if (table.TryGetValue("ToolIds", out _))
+ ConfigurationImportFields.Strings(table, "ToolIds");
+ if (table.TryGetValue("DataSourceOptions", out _))
+ {
+ var options = ConfigurationImportFields.Table(table, "DataSourceOptions");
+ ConfigurationImportFields.Bool(options, "DisableDataSources");
+ ConfigurationImportFields.Bool(options, "AutomaticDataSourceSelection");
+ ConfigurationImportFields.Bool(options, "AutomaticValidation");
+ if (options.TryGetValue("PreselectedDataSourceIds", out _))
+ ConfigurationImportFields.Strings(options, "PreselectedDataSourceIds");
+ }
+ if (!ChatTemplate.TryParseChatTemplateTable(0, table, Guid.Empty, string.Empty, out var parsed) || parsed is not ChatTemplate template)
+ throw new FormatException("The chat template fields are malformed.");
+ var paths = ConfigurationImportFields.Strings(table, "FileAttachments");
+ var validAttachments = new HashSet();
+ var toRelink = new List<(string OriginalPath, string ReplacementPath)>();
+ foreach (var path in paths)
+ {
+ if (ConfigurationImportFields.IsExistingLocalFile(path))
+ validAttachments.Add(FileAttachment.FromPath(path));
+ else
+ toRelink.Add((path, string.Empty));
+ }
+
+ this.DataName = template.Name;
+ this.DataSystemPrompt = template.SystemPrompt;
+ this.PredefinedUserPrompt = template.PredefinedUserPrompt;
+ this.AllowProfileUsage = template.AllowProfileUsage;
+ this.dataExampleConversation = template.ExampleConversation.Select(block => block.DeepClone()).ToList();
+ this.fileAttachments = validAttachments;
+ this.attachmentsToRelink = toRelink;
+ this.preselectTools = template.ToolIds is not null;
+ this.selectedToolIds = template.ToolIds is null ? new(StringComparer.Ordinal) : new(template.ToolIds, StringComparer.Ordinal);
+ this.preselectDataSources = template.DataSourceOptions is not null;
+ this.templateDataSourceOptions = template.DataSourceOptions?.CreateCopy() ?? new DataSourceOptions { DisableDataSources = false };
+ this.importReferenceIssue = await this.BuildImportReferenceIssue();
+ this.form.ResetValidation();
+ }
+
+ private async Task BuildImportReferenceIssue()
+ {
+ var missingSources = this.templateDataSourceOptions.PreselectedDataSourceIds
+ .Where(id => this.SettingsManager.ConfigurationData.DataSources.All(source => source.Id != id)).ToList();
+ var availableToolIds = (await this.ToolRegistry.GetCatalogAsync(AIStudio.Tools.Components.CHAT))
+ .Select(item => item.Definition.Id).ToHashSet(StringComparer.Ordinal);
+ var missingTools = this.selectedToolIds.Where(id => !availableToolIds.Contains(id)).ToList();
+ var missing = missingSources.Select(id => $"data source {id}").Concat(missingTools.Select(id => $"tool {id}")).ToList();
+ return missing.Count == 0 ? string.Empty : $"Unavailable references: {string.Join(", ", missing)}. Review the selection before saving.";
+ }
+
private void SetSelectedToolIds(HashSet toolIds) => this.selectedToolIds = toolIds;
private void RemoveMessage(ContentBlock item)
@@ -271,9 +359,23 @@ public partial class ChatTemplateDialog : MSGComponentBase
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
+ return;
+
if (this.IsReadOnly)
return;
+ this.relinkIssue = string.Empty;
+ foreach (var (originalPath, replacementPath) in this.attachmentsToRelink)
+ {
+ if (!ConfigurationImportFields.IsExistingLocalFile(replacementPath))
+ {
+ this.relinkIssue = string.Format(T("Relink the missing attachment '{0}' to an existing local file before saving."), originalPath);
+ return;
+ }
+ this.fileAttachments.Add(FileAttachment.FromPath(replacementPath));
+ }
+
await this.form.Validate();
// When the data is not valid, we don't store it:
@@ -329,4 +431,4 @@ public partial class ChatTemplateDialog : MSGComponentBase
}
private void Cancel() => this.MudDialog.Cancel();
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor b/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor
new file mode 100644
index 00000000..507ea0bf
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor
@@ -0,0 +1,18 @@
+@inherits MSGComponentBase
+
+
+
+
+ @T("Copy one exported configuration snippet from the item's Export configuration control and paste it below. You can review and change the filled form before saving a new local item.")
+
+
+ @if (!string.IsNullOrWhiteSpace(this.issue))
+ {
+ @this.issue
+ }
+
+
+ @T("Cancel")
+ @this.ImportLabel
+
+
diff --git a/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor.cs
new file mode 100644
index 00000000..c6b97ad5
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/ConfigurationSnippetImportDialog.razor.cs
@@ -0,0 +1,58 @@
+using AIStudio.Components;
+using AIStudio.Tools.PluginSystem;
+
+using Lua;
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs;
+
+public partial class ConfigurationSnippetImportDialog : MSGComponentBase
+{
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public string Section { get; set; } = string.Empty;
+
+ [Parameter]
+ public string ImportLabel { get; set; } = string.Empty;
+
+ private string snippet = string.Empty;
+ private string issue = string.Empty;
+
+ private void Import()
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet(this.Section))
+ {
+ this.issue = T("Import is locked by your organization.");
+ return;
+ }
+
+ if (!ConfigurationSnippetParser.TryParse(this.snippet, this.Section, out var table, out this.issue))
+ return;
+
+ try
+ {
+ ConfigurationSnippetImportValidation.Validate(this.Section, table);
+ this.MudDialog.Close(DialogResult.Ok(table));
+ }
+ catch (FormatException exception)
+ {
+ this.issue = exception.Message;
+ }
+ }
+
+ private void Cancel() => this.MudDialog.Cancel();
+
+ public static async Task ShowAsync(IDialogService service, string section, string title)
+ {
+ var parameters = new DialogParameters
+ {
+ { x => x.Section, section },
+ { x => x.ImportLabel, title },
+ };
+ var dialog = await service.ShowAsync(title, parameters, DialogOptions.FULLSCREEN);
+ var result = await dialog.Result;
+ return result is { Canceled: false, Data: LuaTable table } ? table : null;
+ }
+}
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
index 3a3d4b00..a16077af 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor
@@ -5,6 +5,17 @@
+ @if (!this.IsEditing)
+ {
+ @if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
+ {
+ @this.importCredentialIssue
+ }
+ @if (!string.IsNullOrWhiteSpace(this.importRetrievalIssue))
+ {
+ @this.importRetrievalIssue
+ }
+ }
@* ReSharper disable once CSharpWarnings::CS8974 *@
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
index 8bec772a..26d27855 100644
--- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs
@@ -4,8 +4,11 @@ using AIStudio.Settings.DataModel;
using AIStudio.Tools.ERIClient;
using AIStudio.Tools.ERIClient.DataModel;
using AIStudio.Tools.Services;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Validation;
+using Lua;
+
using Microsoft.AspNetCore.Components;
using RetrievalInfo = AIStudio.Tools.ERIClient.DataModel.RetrievalInfo;
@@ -15,6 +18,9 @@ namespace AIStudio.Dialogs;
public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@@ -43,6 +49,8 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
private bool dataIsValid;
private string[] dataIssues = [];
private string dataSecretStorageIssue = string.Empty;
+ private string importCredentialIssue = string.Empty;
+ private string importRetrievalIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private List availableAuthMethods = [];
private DataSourceSecurity dataSecurityPolicy;
@@ -143,6 +151,15 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
await base.OnAfterRenderAsync(firstRender);
}
@@ -176,6 +193,53 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
MaxMatches = this.dataMaxMatches,
};
}
+
+ private Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ if (ConfigurationImportFields.String(table, "Type") != "ERI_V1")
+ throw new FormatException("This data source is not an ERI v1 data source.");
+ var name = ConfigurationImportFields.String(table, "Name");
+ var hostname = ConfigurationImportFields.String(table, "Hostname");
+ var port = ConfigurationImportFields.Int(table, "Port");
+ if (port is < 1 or > 65535)
+ throw new FormatException("The 'Port' field must be between 1 and 65535.");
+ var authMethod = ConfigurationImportFields.Enum(table, "AuthMethod");
+ if (authMethod is AuthMethod.KERBEROS)
+ throw new FormatException("Kerberos data sources cannot be imported from configuration snippets.");
+ var securityPolicy = ConfigurationImportFields.Enum(table, "SecurityPolicy");
+ var retrievalId = ConfigurationImportFields.String(table, "SelectedRetrievalId");
+ var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
+ if (maxMatches is < 1 or > ushort.MaxValue)
+ throw new FormatException("The 'MaxMatches' field is outside the allowed range.");
+ var secretName = authMethod switch
+ {
+ AuthMethod.TOKEN => "Token",
+ AuthMethod.USERNAME_PASSWORD => "Password",
+ _ => string.Empty,
+ };
+ var secret = string.Empty;
+ var credentialIssue = string.Empty;
+ if (!string.IsNullOrEmpty(secretName))
+ secret = ConfigurationImportFields.Credential(table, secretName, out credentialIssue);
+ var username = ConfigurationImportFields.String(table, "Username", required: false);
+
+ this.dataName = name;
+ this.dataHostname = hostname;
+ this.dataPort = port;
+ this.dataAuthMethod = authMethod;
+ this.dataUsername = username;
+ this.dataSecurityPolicy = securityPolicy;
+ this.dataSelectedRetrievalProcess = this.dataSelectedRetrievalProcess with { Id = retrievalId };
+ this.dataMaxMatches = (ushort)maxMatches;
+ this.dataSecret = secret;
+ this.importCredentialIssue = credentialIssue;
+ this.importRetrievalIssue = string.Empty;
+ this.connectionTested = false;
+ this.connectionSuccessfulTested = false;
+ this.form.ResetValidation();
+ return Task.CompletedTask;
+ }
private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
@@ -251,6 +315,17 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
}
this.availableRetrievalProcesses = retrievalInfoRequest.Data ?? [];
+ if (!string.IsNullOrWhiteSpace(this.dataSelectedRetrievalProcess.Id))
+ {
+ var importedRetrieval = this.availableRetrievalProcesses.FirstOrDefault(item => item.Id == this.dataSelectedRetrievalProcess.Id);
+ if (importedRetrieval != default)
+ this.dataSelectedRetrievalProcess = importedRetrieval;
+ else
+ {
+ this.importRetrievalIssue = string.Format(T("The imported retrieval process '{0}' is unavailable. Select another process before saving."), this.dataSelectedRetrievalProcess.Id);
+ this.dataSelectedRetrievalProcess = default;
+ }
+ }
this.connectionTested = true;
this.connectionSuccessfulTested = true;
@@ -304,6 +379,9 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))
+ return;
+
await this.form.Validate();
var testConnectionValidation = this.dataSourceValidation.ValidateTestedConnection();
@@ -337,4 +415,4 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
}
private void Cancel() => this.MudDialog.Cancel();
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor b/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor
new file mode 100644
index 00000000..72ef518c
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor
@@ -0,0 +1,45 @@
+@using AIStudio.Provider
+@using AIStudio.Settings
+@using AIStudio.Settings.DataModel
+@inherits MSGComponentBase
+
+
+
+ @if (!string.IsNullOrWhiteSpace(this.referenceIssue))
+ {
+ @this.referenceIssue
+ }
+
+
+
+
+
+
+
+ @T("No provider")
+ @foreach (var provider in this.SettingsManager.GetAllProviders())
+ {
+ @provider.InstanceName
+ }
+
+
+ @T("Use app default profile")
+ @T("No profile")
+ @foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
+ {
+ @profile.Name
+ }
+
+
+
+
+
+
+
+
+
+
+ @T("Cancel")
+ @T("Add")
+
+
diff --git a/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor.cs
new file mode 100644
index 00000000..8f03e171
--- /dev/null
+++ b/app/MindWork AI Studio/Dialogs/DocumentAnalysisPolicyDialog.razor.cs
@@ -0,0 +1,134 @@
+using AIStudio.Components;
+using AIStudio.Provider;
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.PluginSystem;
+using AIStudio.Tools.ToolCallingSystem;
+
+using Lua;
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Dialogs;
+
+public partial class DocumentAnalysisPolicyDialog : MSGComponentBase
+{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
+ [CascadingParameter]
+ private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Inject]
+ private ToolRegistry ToolRegistry { get; init; } = null!;
+
+ private MudForm form = null!;
+ private bool isValid;
+ private string[] issues = [];
+ private string referenceIssue = string.Empty;
+ private string name = string.Empty;
+ private string description = string.Empty;
+ private string analysisRules = string.Empty;
+ private string outputRules = string.Empty;
+ private ConfidenceLevel minimumConfidence = ConfidenceLevel.NONE;
+ private HashSet allowedToolIds = new(StringComparer.Ordinal);
+ private string providerId = string.Empty;
+ private string profileId = Profile.NO_PROFILE.Id;
+ private bool hideDefinition;
+ private bool isProtected;
+
+ protected override async Task OnAfterRenderAsync(bool firstRender)
+ {
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
+
+ await base.OnAfterRenderAsync(firstRender);
+ }
+
+ private async Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ var importedName = ConfigurationImportFields.String(table, "PolicyName");
+ var importedDescription = ConfigurationImportFields.String(table, "PolicyDescription");
+ var importedAnalysisRules = ConfigurationImportFields.String(table, "AnalysisRules");
+ var importedOutputRules = ConfigurationImportFields.String(table, "OutputRules");
+ var confidence = ConfigurationImportFields.Enum(table, "MinimumProviderConfidence");
+ var toolIds = ConfigurationImportFields.Strings(table, "AllowedToolIds");
+ var importedProviderId = ConfigurationImportFields.String(table, "PreselectedProvider", required: false);
+ var importedProfileId = ConfigurationImportFields.String(table, "PreselectedProfile", required: false);
+ var hide = ConfigurationImportFields.Bool(table, "HidePolicyDefinition");
+
+ this.name = importedName;
+ this.description = importedDescription;
+ this.analysisRules = importedAnalysisRules;
+ this.outputRules = importedOutputRules;
+ this.minimumConfidence = confidence;
+ this.allowedToolIds = new(toolIds, StringComparer.Ordinal);
+ this.providerId = importedProviderId;
+ this.profileId = importedProfileId;
+ this.hideDefinition = hide;
+
+ var missing = new List();
+ if (!string.IsNullOrWhiteSpace(this.providerId) && this.SettingsManager.GetAllProviders().All(provider => provider.Id != this.providerId))
+ missing.Add($"provider {this.providerId}");
+ if (!string.IsNullOrWhiteSpace(this.profileId) && this.profileId != Profile.NO_PROFILE.Id && this.SettingsManager.ConfigurationData.Profiles.All(profile => profile.Id != this.profileId))
+ missing.Add($"profile {this.profileId}");
+ var availableToolIds = (await this.ToolRegistry.GetCatalogAsync(AIStudio.Tools.Components.DOCUMENT_ANALYSIS_ASSISTANT))
+ .Select(item => item.Definition.Id).ToHashSet(StringComparer.Ordinal);
+ missing.AddRange(this.allowedToolIds.Where(id => !availableToolIds.Contains(id)).Select(id => $"tool {id}"));
+ this.referenceIssue = missing.Count == 0 ? string.Empty : $"Unavailable references: {string.Join(", ", missing)}. Review the selections before saving.";
+ this.form.ResetValidation();
+ }
+
+ private async Task Store()
+ {
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
+ return;
+
+ await this.form.Validate();
+ if (!this.isValid)
+ return;
+
+ this.MudDialog.Close(DialogResult.Ok(new DataDocumentAnalysisPolicy
+ {
+ Id = Guid.NewGuid().ToString(),
+ PolicyName = this.name.Trim(),
+ PolicyDescription = this.description.Trim(),
+ AnalysisRules = this.analysisRules.Trim(),
+ OutputRules = this.outputRules.Trim(),
+ MinimumProviderConfidence = this.minimumConfidence,
+ AllowedToolIds = new(this.allowedToolIds, StringComparer.Ordinal),
+ PreselectedProvider = this.providerId,
+ PreselectedProfile = this.profileId,
+ HidePolicyDefinition = this.hideDefinition,
+ IsProtected = this.isProtected,
+ }));
+ }
+
+ private string? ValidateName(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return T("Please provide a name for your policy. This name will be used to identify the policy in AI Studio.");
+ if (value.Length is < 6 or > 60)
+ return T("The name of your policy must be between 6 and 60 characters long.");
+ if (this.SettingsManager.ConfigurationData.DocumentAnalysis.Policies.Any(policy => policy.PolicyName == value))
+ return T("A policy with this name already exists. Please choose a different name.");
+ return null;
+ }
+
+ private string? ValidateDescription(string value) => value.Length is < 32 or > 512
+ ? T("The description of your policy must be between 32 and 512 characters long.") : null;
+
+ private string? ValidateAnalysisRules(string value) => string.IsNullOrWhiteSpace(value)
+ ? T("Please provide a description of your analysis rules. This rules will be used to instruct the AI on how to analyze the documents.") : null;
+
+ private string? ValidateOutputRules(string value) => string.IsNullOrWhiteSpace(value)
+ ? T("Please provide a description of your output rules. This rules will be used to instruct the AI on how to format the output of the analysis.") : null;
+
+ private void Cancel() => this.MudDialog.Cancel();
+}
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
index 566e0640..26357d1e 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor
@@ -5,6 +5,13 @@
+ @if (!this.IsEditing)
+ {
+ @if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
+ {
+ @this.importCredentialIssue
+ }
+ }
@if (this.IsEnterpriseConfiguration)
{
diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
index 765e7aa5..b290481b 100644
--- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs
@@ -4,8 +4,11 @@ using AIStudio.Provider.HuggingFace;
using AIStudio.Settings;
using AIStudio.Tools.Rust;
using AIStudio.Tools.Services;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Validation;
+using Lua;
+
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Host = AIStudio.Provider.SelfHosted.Host;
@@ -14,6 +17,9 @@ namespace AIStudio.Dialogs;
public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@@ -134,6 +140,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataAPIKeyStorageIssue = string.Empty;
+ private string importCredentialIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private string dataLoadingModelsIssue = string.Empty;
private bool dataConfiguredModelIsNotOffered;
@@ -190,6 +197,40 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
HFInferenceProvider = this.HFInferenceProviderId,
};
}
+
+ private Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ var modelTable = ConfigurationImportFields.Table(table, "Model");
+ var model = new Model(ConfigurationImportFields.String(modelTable, "Id"), ConfigurationImportFields.String(modelTable, "DisplayName"));
+ var name = ConfigurationImportFields.String(table, "Name");
+ var provider = ConfigurationImportFields.Enum(table, "UsedLLMProvider");
+ var host = ConfigurationImportFields.Enum(table, "Host");
+ var hostname = ConfigurationImportFields.String(table, "Hostname");
+ var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
+ ? ConfigurationImportFields.Enum(table, "HFInferenceProvider") : HFInferenceProvider.NONE;
+ var tokenizerPath = ConfigurationImportFields.String(table, "TokenizerPath", required: false);
+ var tokenLimit = ConfigurationImportFields.Int(table, "TokenLimit", EmbeddingProvider.DEFAULT_TOKEN_LIMIT);
+ var batchSize = ConfigurationImportFields.Int(table, "EmbeddingBatchSize", EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE);
+ var credential = ConfigurationImportFields.Credential(table, "APIKey", out var credentialIssue);
+
+ this.DataName = name;
+ this.DataLLMProvider = provider;
+ this.DataHost = host;
+ this.DataHostname = hostname;
+ this.HFInferenceProviderId = hfProvider;
+ this.DataModel = model;
+ this.dataFilePath = tokenizerPath;
+ this.DataTokenLimit = tokenLimit;
+ this.DataEmbeddingBatchSize = batchSize;
+ this.showExpertSettings = !string.IsNullOrWhiteSpace(tokenizerPath) || tokenLimit != EmbeddingProvider.DEFAULT_TOKEN_LIMIT || batchSize != EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE;
+ this.dataAPIKey = credential;
+ this.importCredentialIssue = credentialIssue;
+ if (this.availableModels.All(candidate => candidate.Id != model.Id))
+ this.availableModels.Add(model);
+ this.form.ResetValidation();
+ return Task.CompletedTask;
+ }
#region Overrides of ComponentBase
@@ -247,6 +288,15 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
await base.OnAfterRenderAsync(firstRender);
}
@@ -267,6 +317,9 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))
+ return;
+
this.dataStoreWasAttempted = true;
await this.dataTokenizerValidationTask;
await this.form.Validate();
diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
index b72d3135..c62103ec 100644
--- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor
@@ -99,4 +99,4 @@
}
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
index ba2dfff8..2e6dc0ac 100644
--- a/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ProfileDialog.razor.cs
@@ -1,5 +1,8 @@
using AIStudio.Components;
using AIStudio.Settings;
+using AIStudio.Tools.PluginSystem;
+
+using Lua;
using Microsoft.AspNetCore.Components;
@@ -7,6 +10,9 @@ namespace AIStudio.Dialogs;
public partial class ProfileDialog : MSGComponentBase
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@@ -78,6 +84,19 @@ public partial class ProfileDialog : MSGComponentBase
IsEnterpriseConfiguration = false,
};
+ private Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ var name = ConfigurationImportFields.String(table, "Name");
+ var needToKnow = ConfigurationImportFields.String(table, "NeedToKnow");
+ var actions = ConfigurationImportFields.String(table, "Actions");
+ this.DataName = name;
+ this.DataNeedToKnow = needToKnow;
+ this.DataActions = actions;
+ this.form.ResetValidation();
+ return Task.CompletedTask;
+ }
+
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
@@ -104,6 +123,15 @@ public partial class ProfileDialog : MSGComponentBase
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
+
await base.OnAfterRenderAsync(firstRender);
}
@@ -111,6 +139,9 @@ public partial class ProfileDialog : MSGComponentBase
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))
+ return;
+
if (this.IsReadOnly)
return;
@@ -165,4 +196,4 @@ public partial class ProfileDialog : MSGComponentBase
}
private void Cancel() => this.MudDialog.Cancel();
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
index be647902..87a94410 100644
--- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor
@@ -4,6 +4,13 @@
@inherits MSGComponentBase
+ @if (!this.IsEditing)
+ {
+ @if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
+ {
+ @this.importCredentialIssue
+ }
+ }
@if (this.IsEnterpriseConfiguration)
{
diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
index 26666691..114110ea 100644
--- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs
@@ -10,8 +10,11 @@ using AIStudio.Tools.Rust;
using AIStudio.Settings;
using AIStudio.Settings.DataModel;
using AIStudio.Tools.Services;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Validation;
+using Lua;
+
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@@ -24,6 +27,9 @@ namespace AIStudio.Dialogs;
///
public partial class ProviderDialog : MSGComponentBase, ISecretId
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
private enum ReasoningOverrideMode
{
AUTOMATIC,
@@ -154,6 +160,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
private bool dataHadStoredAPIKeyOnLoad;
private string dataManuallyModel = string.Empty;
private string dataAPIKeyStorageIssue = string.Empty;
+ private string importCredentialIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private string dataLoadingModelsIssue = string.Empty;
private string dataFilePath = string.Empty;
@@ -232,6 +239,41 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
return this.DataModel;
}
+ private Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ var modelTable = ConfigurationImportFields.Table(table, "Model");
+ var model = new Model(ConfigurationImportFields.String(modelTable, "Id"), ConfigurationImportFields.String(modelTable, "DisplayName"));
+ var name = ConfigurationImportFields.String(table, "InstanceName");
+ var provider = ConfigurationImportFields.Enum(table, "UsedLLMProvider");
+ var host = ConfigurationImportFields.Enum(table, "Host");
+ var hostname = ConfigurationImportFields.String(table, "Hostname");
+ var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
+ ? ConfigurationImportFields.Enum(table, "HFInferenceProvider") : HFInferenceProvider.NONE;
+ var tokenizerPath = ConfigurationImportFields.String(table, "TokenizerPath", required: false);
+ var additionalParameters = ConfigurationImportFields.String(table, "AdditionalJsonApiParameters", required: false);
+ var credential = ConfigurationImportFields.Credential(table, "APIKey", out var credentialIssue);
+ var overrides = ProviderCapabilityOverrides.TryParseFromLuaTable(0, table, Guid.Empty, this.Logger);
+
+ this.DataInstanceName = name;
+ this.DataLLMProvider = provider;
+ this.DataHost = host;
+ this.DataHostname = hostname;
+ this.HFInferenceProviderId = hfProvider;
+ this.DataModel = model;
+ this.dataManuallyModel = model.Id;
+ this.dataFilePath = tokenizerPath;
+ this.AdditionalJsonApiParameters = additionalParameters;
+ this.capabilityOverrides = overrides ?? new();
+ this.showExpertSettings = this.capabilityOverrides.HasOverrides || !string.IsNullOrWhiteSpace(this.dataFilePath) || !string.IsNullOrWhiteSpace(this.AdditionalJsonApiParameters);
+ this.dataAPIKey = credential;
+ this.importCredentialIssue = credentialIssue;
+ if (this.availableModels.All(candidate => candidate.Id != model.Id))
+ this.availableModels.Add(model);
+ this.form.ResetValidation();
+ return Task.CompletedTask;
+ }
+
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync()
@@ -297,6 +339,15 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
await base.OnAfterRenderAsync(firstRender);
}
@@ -317,6 +368,9 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))
+ return;
+
this.dataStoreWasAttempted = true;
await this.dataTokenizerValidationTask;
await this.form.Validate();
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor
index 305f25e5..b6060a17 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor
@@ -81,13 +81,16 @@
}
-
- @T("Add Chat Template")
-
+
+
+ @T("Add Chat Template")
+
+
+
@T("Close")
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs
index b941beab..d66ab0f0 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogChatTemplate.razor.cs
@@ -2,6 +2,8 @@ using AIStudio.Chat;
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
+using Lua;
+
namespace AIStudio.Dialogs.Settings;
public partial class SettingsDialogChatTemplate : SettingsDialogBase
@@ -26,14 +28,30 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
#endregion
- private async Task AddChatTemplate()
+ private Task AddChatTemplate() => this.AddChatTemplate(null);
+
+ private async Task ImportChatTemplate()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "CHAT_TEMPLATES", T("Import Chat Template"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
+ await this.AddChatTemplate(table);
+ }
+
+ private async Task AddChatTemplate(LuaTable? importedConfiguration)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
- if (this.CreateTemplateFromExistingChatThread)
+ if (this.CreateTemplateFromExistingChatThread && importedConfiguration is null)
{
dialogParameters.Add(x => x.CreateFromExistingChatThread, this.CreateTemplateFromExistingChatThread);
dialogParameters.Add(x => x.ExistingChatThread, this.ExistingChatThread);
@@ -41,7 +59,8 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
var dialogReference = await this.DialogService.ShowAsync(T("Add Chat Template"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
- if (dialogResult is null || dialogResult.Canceled)
+ if (dialogResult is null || dialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES")))
return;
var addedChatTemplate = (ChatTemplate)dialogResult.Data!;
@@ -229,4 +248,4 @@ public partial class SettingsDialogChatTemplate : SettingsDialogBase
if (!string.IsNullOrWhiteSpace(luaCode))
await this.RustService.CopyText2Clipboard(luaCode);
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor
index f84a170b..6b24a70a 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor
@@ -64,13 +64,16 @@
}
-
- @T("Add Profile")
-
+
+
+ @T("Add Profile")
+
+
+
@T("Close")
-
\ No newline at end of file
+
diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs
index 531583a0..f274eeb7 100644
--- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogProfiles.razor.cs
@@ -1,19 +1,38 @@
using AIStudio.Settings;
+using Lua;
+
namespace AIStudio.Dialogs.Settings;
public partial class SettingsDialogProfiles : SettingsDialogBase
{
- private async Task AddProfile()
+ private Task AddProfile() => this.AddProfile(null);
+
+ private async Task ImportProfile()
{
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))
+ return;
+ var table = await ConfigurationSnippetImportDialog.ShowAsync(this.DialogService, "PROFILES", T("Import Profile"));
+ if (table is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))
+ await this.AddProfile(table);
+ }
+
+ private async Task AddProfile(LuaTable? importedConfiguration)
+ {
+ if (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))
+ return;
+
var dialogParameters = new DialogParameters
{
{ x => x.IsEditing, false },
};
+ if (importedConfiguration is not null)
+ dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync(T("Add Profile"), dialogParameters, DialogOptions.FULLSCREEN);
var dialogResult = await dialogReference.Result;
- if (dialogResult is null || dialogResult.Canceled)
+ if (dialogResult is null || dialogResult.Canceled ||
+ (importedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES")))
return;
var addedProfile = (Profile)dialogResult.Data!;
@@ -95,4 +114,4 @@ public partial class SettingsDialogProfiles : SettingsDialogBase
await this.MessageBus.SendMessage(this, Event.CONFIGURATION_CHANGED);
}
-}
\ No newline at end of file
+}
diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
index 003129b2..ed1f8164 100644
--- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
+++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor
@@ -5,6 +5,13 @@
+ @if (!this.IsEditing)
+ {
+ @if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
+ {
+ @this.importCredentialIssue
+ }
+ }
@if (this.IsEnterpriseConfiguration)
{
diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
index dd463a06..72a7a605 100644
--- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
+++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs
@@ -3,8 +3,11 @@ using AIStudio.Provider;
using AIStudio.Provider.HuggingFace;
using AIStudio.Settings;
using AIStudio.Tools.Services;
+using AIStudio.Tools.PluginSystem;
using AIStudio.Tools.Validation;
+using Lua;
+
using Microsoft.AspNetCore.Components;
using Host = AIStudio.Provider.SelfHosted.Host;
@@ -13,6 +16,9 @@ namespace AIStudio.Dialogs;
public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
{
+ [Parameter]
+ public LuaTable? ImportedConfiguration { get; set; }
+
[CascadingParameter]
private IMudDialogInstance MudDialog { get; set; } = null!;
@@ -107,6 +113,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
private string dataAPIKey = string.Empty;
private bool dataHadStoredAPIKeyOnLoad;
private string dataAPIKeyStorageIssue = string.Empty;
+ private string importCredentialIssue = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private string dataLoadingModelsIssue = string.Empty;
@@ -153,6 +160,33 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
HFInferenceProvider = this.HFInferenceProviderId,
};
}
+
+ private Task ImportConfiguration(LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ var modelTable = ConfigurationImportFields.Table(table, "Model");
+ var model = new Model(ConfigurationImportFields.String(modelTable, "Id"), ConfigurationImportFields.String(modelTable, "DisplayName"));
+ var name = ConfigurationImportFields.String(table, "Name");
+ var provider = ConfigurationImportFields.Enum(table, "UsedLLMProvider");
+ var host = ConfigurationImportFields.Enum(table, "Host");
+ var hostname = ConfigurationImportFields.String(table, "Hostname");
+ var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
+ ? ConfigurationImportFields.Enum(table, "HFInferenceProvider") : HFInferenceProvider.NONE;
+ var credential = ConfigurationImportFields.Credential(table, "APIKey", out var credentialIssue);
+
+ this.DataName = name;
+ this.DataLLMProvider = provider;
+ this.DataHost = host;
+ this.DataHostname = hostname;
+ this.HFInferenceProviderId = hfProvider;
+ this.DataModel = model;
+ this.dataAPIKey = credential;
+ this.importCredentialIssue = credentialIssue;
+ if (this.availableModels.All(candidate => candidate.Id != model.Id))
+ this.availableModels.Add(model);
+ this.form.ResetValidation();
+ return Task.CompletedTask;
+ }
#region Overrides of ComponentBase
@@ -205,6 +239,15 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
// We don't want to show validation errors when the user opens the dialog.
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
+
+ if (firstRender && this.ImportedConfiguration is not null)
+ {
+ if (!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))
+ this.MudDialog.Cancel();
+ else
+ await this.ImportConfiguration(this.ImportedConfiguration);
+ this.StateHasChanged();
+ }
await base.OnAfterRenderAsync(firstRender);
}
@@ -225,6 +268,9 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
private async Task Store()
{
+ if (this.ImportedConfiguration is not null && !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))
+ return;
+
await this.form.Validate();
this.dataAPIKeyStorageIssue = string.Empty;
diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
index 61590c55..150c2ab5 100644
--- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua
+++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua
@@ -395,6 +395,20 @@ CONFIG["SETTINGS"] = {}
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddEmbeddingProvider"] = false
-- CONFIG["SETTINGS"]["DataApp.AllowUserToAddTranscriptionProvider"] = false
+-- Control whether users may paste exported configuration snippets to create local items.
+-- The master setting and the matching item setting must both be true. Provider imports
+-- also require DataApp.AllowUserToAddProvider and the matching provider Add setting.
+-- Blocked Import buttons remain visible with a lock. These settings do not affect
+-- plugin archive imports or exports.
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportConfigurationSnippets"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportProfile"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportLLMProvider"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportEmbeddingProvider"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportTranscriptionProvider"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportChatTemplate"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportERIDataSource"] = false
+-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportDocumentAnalysisPolicy"] = false
+
-- Configure the user permission to import plugin archives from disk.
-- When set to false, the import button on the plugins page stays visible but is disabled.
-- CONFIG["SETTINGS"]["DataApp.AllowUserToImportPlugins"] = false
diff --git a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
index fd485d76..298cf00f 100644
--- a/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
+++ b/app/MindWork AI Studio/Settings/DataModel/DataApp.cs
@@ -181,6 +181,29 @@ public sealed class DataApp(Expression>? configSelection = n
///
public bool AllowUserToAddTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddTranscriptionProvider, true);
+ /// Should the user be allowed to import exported configuration snippets?
+ public bool AllowUserToImportConfigurationSnippets { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportConfigurationSnippets, true);
+
+ public bool AllowUserToImportProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportProfile, true);
+ public bool AllowUserToImportLLMProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportLLMProvider, true);
+ public bool AllowUserToImportEmbeddingProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportEmbeddingProvider, true);
+ public bool AllowUserToImportTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportTranscriptionProvider, true);
+ public bool AllowUserToImportChatTemplate { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportChatTemplate, true);
+ public bool AllowUserToImportERIDataSource { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportERIDataSource, true);
+ public bool AllowUserToImportDocumentAnalysisPolicy { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToImportDocumentAnalysisPolicy, true);
+
+ public bool CanImportConfigurationSnippet(string section) => this.AllowUserToImportConfigurationSnippets && (section switch
+ {
+ "PROFILES" => this.AllowUserToImportProfile,
+ "LLM_PROVIDERS" => this.AllowUserToImportLLMProvider && this.AllowUserToAddProvider && this.AllowUserToAddLLMProvider,
+ "EMBEDDING_PROVIDERS" => this.AllowUserToImportEmbeddingProvider && this.AllowUserToAddProvider && this.AllowUserToAddEmbeddingProvider,
+ "TRANSCRIPTION_PROVIDERS" => this.AllowUserToImportTranscriptionProvider && this.AllowUserToAddProvider && this.AllowUserToAddTranscriptionProvider,
+ "CHAT_TEMPLATES" => this.AllowUserToImportChatTemplate,
+ "DATA_SOURCES" => this.AllowUserToImportERIDataSource,
+ "DOCUMENT_ANALYSIS_POLICIES" => this.AllowUserToImportDocumentAnalysisPolicy,
+ _ => false,
+ });
+
///
/// Should the user be allowed to import plugin archives from disk?
///
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationImportFields.cs b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationImportFields.cs
new file mode 100644
index 00000000..18811ff8
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationImportFields.cs
@@ -0,0 +1,90 @@
+using AIStudio.Tools;
+
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+public static class ConfigurationImportFields
+{
+ public static void ValidateExportId(LuaTable table)
+ {
+ if (!Guid.TryParse(String(table, "Id"), out _))
+ throw new FormatException("The exported item has an invalid ID.");
+ }
+
+ public static string String(LuaTable table, string name, bool required = true)
+ {
+ if (table.TryGetValue(name, out var value))
+ {
+ if (value.Type is LuaValueType.String && value.TryRead(out var text))
+ return text;
+ throw new FormatException($"The '{name}' field must be a string.");
+ }
+ if (!required)
+ return string.Empty;
+ throw new FormatException($"The '{name}' field must be a string.");
+ }
+
+ public static LuaTable Table(LuaTable table, string name)
+ {
+ if (table.TryGetValue(name, out var value) && value.Type is LuaValueType.Table && value.TryRead(out var nested))
+ return nested;
+ throw new FormatException($"The '{name}' field must be a table.");
+ }
+
+ public static T Enum(LuaTable table, string name) where T : struct, Enum
+ {
+ var text = String(table, name);
+ if (System.Enum.TryParse(text, true, out var result) && System.Enum.IsDefined(result))
+ return result;
+ throw new FormatException($"The '{name}' field has an unknown value.");
+ }
+
+ public static bool Bool(LuaTable table, string name, bool fallback = false)
+ {
+ if (!table.TryGetValue(name, out var value))
+ return fallback;
+ if (value.Type is LuaValueType.Boolean && value.TryRead(out var result))
+ return result;
+ throw new FormatException($"The '{name}' field must be true or false.");
+ }
+
+ public static int Int(LuaTable table, string name, int fallback = 0)
+ {
+ if (!table.TryGetValue(name, out var value))
+ return fallback;
+ if (value.Type is LuaValueType.Number && value.TryRead(out var number) && number >= int.MinValue && number <= int.MaxValue && number == Math.Truncate(number))
+ return (int)number;
+ throw new FormatException($"The '{name}' field must be a whole number.");
+ }
+
+ public static List Strings(LuaTable table, string name)
+ {
+ var nested = Table(table, name);
+ var result = new List();
+ for (var i = 1; i <= nested.ArrayLength; i++)
+ {
+ if (nested[i].Type is not LuaValueType.String || !nested[i].TryRead(out var text) || string.IsNullOrWhiteSpace(text))
+ throw new FormatException($"The '{name}' field contains an invalid entry.");
+ result.Add(text);
+ }
+ return result;
+ }
+
+ public static bool IsExistingLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path);
+
+ public static string Credential(LuaTable table, string name, out string issue, EnterpriseEncryption? decryptionService = null)
+ {
+ issue = string.Empty;
+ var encrypted = String(table, name, required: false);
+ if (string.IsNullOrEmpty(encrypted))
+ return string.Empty;
+ if (!EnterpriseEncryption.IsEncrypted(encrypted))
+ throw new FormatException($"The '{name}' field must contain an ENC:v1 credential.");
+ var encryption = decryptionService ?? PluginFactory.EnterpriseEncryption;
+ if (encryption?.IsAvailable == true && encryption.TryDecrypt(encrypted, out var decrypted))
+ return decrypted;
+ issue = "The embedded credential could not be decrypted on this device. Enter your own credential before saving.";
+ return string.Empty;
+ }
+}
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImportValidation.cs b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImportValidation.cs
new file mode 100644
index 00000000..6e8a5a32
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetImportValidation.cs
@@ -0,0 +1,135 @@
+using AIStudio.Chat;
+using AIStudio.Provider;
+using AIStudio.Provider.HuggingFace;
+using AIStudio.Settings;
+using AIStudio.Settings.DataModel;
+using AIStudio.Tools.ERIClient.DataModel;
+
+using Lua;
+
+using Host = AIStudio.Provider.SelfHosted.Host;
+
+namespace AIStudio.Tools.PluginSystem;
+
+/// Checks the fields used by the creation forms before leaving the paste dialog.
+public static class ConfigurationSnippetImportValidation
+{
+ public static void Validate(string section, LuaTable table)
+ {
+ ConfigurationImportFields.ValidateExportId(table);
+ switch (section)
+ {
+ case "PROFILES":
+ ConfigurationImportFields.String(table, "Name");
+ ConfigurationImportFields.String(table, "NeedToKnow");
+ ConfigurationImportFields.String(table, "Actions");
+ break;
+ case "LLM_PROVIDERS":
+ case "EMBEDDING_PROVIDERS":
+ case "TRANSCRIPTION_PROVIDERS":
+ ValidateProvider(section, table);
+ break;
+ case "CHAT_TEMPLATES":
+ ValidateChatTemplate(table);
+ break;
+ case "DATA_SOURCES":
+ ValidateERIDataSource(table);
+ break;
+ case "DOCUMENT_ANALYSIS_POLICIES":
+ ConfigurationImportFields.String(table, "PolicyName");
+ ConfigurationImportFields.String(table, "PolicyDescription");
+ ConfigurationImportFields.String(table, "AnalysisRules");
+ ConfigurationImportFields.String(table, "OutputRules");
+ ConfigurationImportFields.Enum(table, "MinimumProviderConfidence");
+ ConfigurationImportFields.Strings(table, "AllowedToolIds");
+ ConfigurationImportFields.String(table, "PreselectedProvider", required: false);
+ ConfigurationImportFields.String(table, "PreselectedProfile", required: false);
+ ConfigurationImportFields.Bool(table, "HidePolicyDefinition");
+ break;
+ default:
+ throw new FormatException("This configuration section cannot be imported here.");
+ }
+ }
+
+ private static void ValidateProvider(string section, LuaTable table)
+ {
+ var model = ConfigurationImportFields.Table(table, "Model");
+ ConfigurationImportFields.String(model, "Id");
+ ConfigurationImportFields.String(model, "DisplayName");
+ ConfigurationImportFields.String(table, section == "LLM_PROVIDERS" ? "InstanceName" : "Name");
+ ConfigurationImportFields.Enum(table, "UsedLLMProvider");
+ ConfigurationImportFields.Enum(table, "Host");
+ ConfigurationImportFields.String(table, "Hostname");
+ if (table.TryGetValue("HFInferenceProvider", out _))
+ ConfigurationImportFields.Enum(table, "HFInferenceProvider");
+ if (section != "TRANSCRIPTION_PROVIDERS")
+ ConfigurationImportFields.String(table, "TokenizerPath", required: false);
+ if (section == "LLM_PROVIDERS")
+ ConfigurationImportFields.String(table, "AdditionalJsonApiParameters", required: false);
+ if (section == "EMBEDDING_PROVIDERS")
+ {
+ ConfigurationImportFields.Int(table, "TokenLimit", EmbeddingProvider.DEFAULT_TOKEN_LIMIT);
+ ConfigurationImportFields.Int(table, "EmbeddingBatchSize", EmbeddingProvider.DEFAULT_EMBEDDING_BATCH_SIZE);
+ }
+ ConfigurationImportFields.Credential(table, "APIKey", out _);
+ }
+
+ private static void ValidateChatTemplate(LuaTable table)
+ {
+ ConfigurationImportFields.String(table, "Name");
+ ConfigurationImportFields.String(table, "SystemPrompt");
+ ConfigurationImportFields.String(table, "PredefinedUserPrompt", required: false);
+ ConfigurationImportFields.Bool(table, "AllowProfileUsage");
+ var messages = ConfigurationImportFields.Table(table, "ExampleConversation");
+ for (var index = 1; index <= messages.ArrayLength; index++)
+ {
+ if (messages[index].Type is not LuaValueType.Table || !messages[index].TryRead(out var message))
+ throw new FormatException("An example conversation entry is not a table.");
+ ConfigurationImportFields.Enum(message, "Role");
+ if (string.IsNullOrWhiteSpace(ConfigurationImportFields.String(message, "Content")))
+ throw new FormatException("An example conversation message is empty.");
+ }
+ if (table.TryGetValue("ToolIds", out _))
+ ConfigurationImportFields.Strings(table, "ToolIds");
+ if (table.TryGetValue("DataSourceOptions", out _))
+ {
+ var options = ConfigurationImportFields.Table(table, "DataSourceOptions");
+ ConfigurationImportFields.Bool(options, "DisableDataSources");
+ ConfigurationImportFields.Bool(options, "AutomaticDataSourceSelection");
+ ConfigurationImportFields.Bool(options, "AutomaticValidation");
+ if (options.TryGetValue("PreselectedDataSourceIds", out _))
+ ConfigurationImportFields.Strings(options, "PreselectedDataSourceIds");
+ }
+ if (!ChatTemplate.TryParseChatTemplateTable(0, table, Guid.Empty, string.Empty, out _))
+ throw new FormatException("The chat template fields are malformed.");
+ ConfigurationImportFields.Strings(table, "FileAttachments");
+ }
+
+ private static void ValidateERIDataSource(LuaTable table)
+ {
+ if (ConfigurationImportFields.String(table, "Type") != "ERI_V1")
+ throw new FormatException("This data source is not an ERI v1 data source.");
+ ConfigurationImportFields.String(table, "Name");
+ ConfigurationImportFields.String(table, "Hostname");
+ var port = ConfigurationImportFields.Int(table, "Port");
+ if (port is < 1 or > 65535)
+ throw new FormatException("The 'Port' field must be between 1 and 65535.");
+ var authMethod = ConfigurationImportFields.Enum(table, "AuthMethod");
+ if (authMethod is AuthMethod.KERBEROS)
+ throw new FormatException("Kerberos data sources cannot be imported from configuration snippets.");
+ ConfigurationImportFields.Enum(table, "SecurityPolicy");
+ ConfigurationImportFields.String(table, "SelectedRetrievalId");
+ var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
+ if (maxMatches is < 1 or > ushort.MaxValue)
+ throw new FormatException("The 'MaxMatches' field is outside the allowed range.");
+ var secretName = authMethod switch
+ {
+ AuthMethod.TOKEN => "Token",
+ AuthMethod.USERNAME_PASSWORD => "Password",
+ _ => string.Empty,
+ };
+ if (!string.IsNullOrEmpty(secretName))
+ ConfigurationImportFields.Credential(table, secretName, out _);
+ ConfigurationImportFields.String(table, "Username", required: false);
+ }
+}
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetParser.cs b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetParser.cs
new file mode 100644
index 00000000..f036bb64
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/PluginSystem/ConfigurationSnippetParser.cs
@@ -0,0 +1,235 @@
+using System.Globalization;
+using System.Text;
+
+using Lua;
+
+namespace AIStudio.Tools.PluginSystem;
+
+/// Reads one exported configuration assignment as data. No Lua state is created.
+public sealed class ConfigurationSnippetParser
+{
+ private readonly string source;
+ private int position;
+
+ private ConfigurationSnippetParser(string source) => this.source = source;
+
+ public static bool TryParse(string source, string expectedSection, out LuaTable table, out string issue)
+ {
+ table = new LuaTable();
+ issue = string.Empty;
+ if (string.IsNullOrWhiteSpace(source) || source.Length > 1_000_000)
+ {
+ issue = "Paste one exported configuration snippet (up to 1 MB).";
+ return false;
+ }
+
+ try
+ {
+ var parser = new ConfigurationSnippetParser(source);
+ parser.ExpectWord("CONFIG");
+ var section = parser.ReadBracketedString();
+ if (section != expectedSection)
+ throw new FormatException($"This is a {section} snippet. Paste a {expectedSection} snippet here.");
+
+ parser.Expect('[');
+ parser.Expect('#');
+ parser.ExpectWord("CONFIG");
+ if (parser.ReadBracketedString() != expectedSection)
+ throw new FormatException("The configuration section names do not match.");
+ parser.Expect('+');
+ parser.Expect('1');
+ parser.Expect(']');
+ parser.Expect('=');
+ table = parser.ReadTable(0);
+ parser.SkipTrivia();
+ if (parser.position != source.Length)
+ throw new FormatException("The snippet must contain exactly one table assignment and no executable code.");
+ return true;
+ }
+ catch (FormatException exception)
+ {
+ issue = exception.Message;
+ return false;
+ }
+ }
+
+ private LuaTable ReadTable(int depth)
+ {
+ if (depth > 32)
+ throw new FormatException("The snippet contains too many nested tables.");
+ this.Expect('{');
+ var table = new LuaTable();
+ var arrayIndex = 1;
+ var names = new HashSet(StringComparer.Ordinal);
+ while (true)
+ {
+ this.SkipTrivia();
+ if (this.Take('}'))
+ return table;
+
+ if (this.Take('['))
+ {
+ var key = this.ReadString();
+ this.Expect(']');
+ this.Expect('=');
+ if (!names.Add(key))
+ throw new FormatException($"The field '{key}' occurs more than once.");
+ table[key] = this.ReadValue(depth + 1);
+ }
+ else
+ table[arrayIndex++] = this.ReadValue(depth + 1);
+
+ this.SkipTrivia();
+ if (this.Take('}'))
+ return table;
+ if (!this.Take(',') && !this.Take(';'))
+ throw new FormatException($"Expected a comma or closing brace at character {this.position + 1}.");
+ }
+ }
+
+ private LuaValue ReadValue(int depth)
+ {
+ this.SkipTrivia();
+ if (this.Peek() == '{')
+ return this.ReadTable(depth);
+ if (this.Peek() is '"' or '\'' || this.Peek() == '[')
+ return this.ReadString();
+ if (this.TakeWord("true"))
+ return true;
+ if (this.TakeWord("false"))
+ return false;
+ if (this.TakeWord("nil"))
+ return LuaValue.Nil;
+
+ var start = this.position;
+ if (this.Peek() == '-')
+ this.position++;
+ while (char.IsAsciiDigit(this.Peek()))
+ this.position++;
+ if (this.Peek() == '.')
+ {
+ this.position++;
+ while (char.IsAsciiDigit(this.Peek()))
+ this.position++;
+ }
+ if (this.position > start && double.TryParse(this.source[start..this.position], NumberStyles.Float, CultureInfo.InvariantCulture, out var number) && double.IsFinite(number))
+ return number;
+ throw new FormatException($"Only literal values are allowed at character {start + 1}; executable Lua is not accepted.");
+ }
+
+ private string ReadBracketedString()
+ {
+ this.Expect('[');
+ var result = this.ReadString();
+ this.Expect(']');
+ return result;
+ }
+
+ private string ReadString()
+ {
+ this.SkipTrivia();
+ var quote = this.Peek();
+ if (quote == '[')
+ {
+ this.position++;
+ var equalsStart = this.position;
+ while (this.Peek() == '=')
+ this.position++;
+ var equals = this.source[equalsStart..this.position];
+ this.Expect('[');
+ if (this.Peek() is '\r' or '\n')
+ {
+ if (this.Take('\r'))
+ this.Take('\n');
+ else
+ this.position++;
+ }
+ var end = this.source.IndexOf("]" + equals + "]", this.position, StringComparison.Ordinal);
+ if (end < 0)
+ throw new FormatException("Unterminated long string.");
+ var value = this.source[this.position..end];
+ this.position = end + equals.Length + 2;
+ return value;
+ }
+ if (quote is not ('"' or '\''))
+ throw new FormatException($"Expected a quoted string at character {this.position + 1}.");
+ this.position++;
+ var builder = new StringBuilder();
+ while (this.position < this.source.Length)
+ {
+ var c = this.source[this.position++];
+ if (c == quote)
+ return builder.ToString();
+ if (c is '\r' or '\n')
+ throw new FormatException("A quoted string contains an unescaped newline.");
+ if (c != '\\')
+ {
+ builder.Append(c);
+ continue;
+ }
+ if (this.position == this.source.Length)
+ break;
+ c = this.source[this.position++];
+ builder.Append(c switch
+ {
+ 'n' => '\n', 'r' => '\r', 't' => '\t', 'a' => '\a', 'b' => '\b', 'f' => '\f', 'v' => '\v',
+ '\\' => '\\', '"' => '"', '\'' => '\'',
+ _ => throw new FormatException($"Unsupported string escape \\{c}."),
+ });
+ }
+ throw new FormatException("Unterminated quoted string.");
+ }
+
+ private void SkipTrivia()
+ {
+ while (this.position < this.source.Length)
+ {
+ if (char.IsWhiteSpace(this.source[this.position]))
+ {
+ this.position++;
+ continue;
+ }
+ if (this.source.AsSpan(this.position).StartsWith("--"))
+ {
+ this.position += 2;
+ while (this.position < this.source.Length && this.source[this.position] != '\n')
+ this.position++;
+ continue;
+ }
+ break;
+ }
+ }
+
+ private char Peek() => this.position < this.source.Length ? this.source[this.position] : '\0';
+
+ private bool Take(char c)
+ {
+ this.SkipTrivia();
+ if (this.Peek() != c)
+ return false;
+ this.position++;
+ return true;
+ }
+
+ private void Expect(char c)
+ {
+ if (!this.Take(c))
+ throw new FormatException($"Expected '{c}' at character {this.position + 1}.");
+ }
+
+ private bool TakeWord(string word)
+ {
+ this.SkipTrivia();
+ if (!this.source.AsSpan(this.position).StartsWith(word) ||
+ (this.position + word.Length < this.source.Length && (char.IsLetterOrDigit(this.source[this.position + word.Length]) || this.source[this.position + word.Length] == '_')))
+ return false;
+ this.position += word.Length;
+ return true;
+ }
+
+ private void ExpectWord(string word)
+ {
+ if (!this.TakeWord(word))
+ throw new FormatException($"Expected '{word}' at character {this.position + 1}.");
+ }
+}
diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
index ea02f556..bb763e6f 100644
--- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
+++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs
@@ -250,6 +250,15 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: allow the user to add transcription providers?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToAddTranscriptionProvider, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportConfigurationSnippets, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportProfile, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportLLMProvider, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportEmbeddingProvider, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportTranscriptionProvider, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportChatTemplate, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportERIDataSource, this.Id, settingsTable, dryRun);
+ ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportDocumentAnalysisPolicy, this.Id, settingsTable, dryRun);
+
// Config: allow the user to import plugin archives?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.AllowUserToImportPlugins, this.Id, settingsTable, dryRun);
diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
index e7affb11..83ed9be8 100644
--- a/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
+++ b/app/MindWork AI Studio/wwwroot/changelog/v26.9.1.md
@@ -1,4 +1,7 @@
# v26.9.1, build 256 (2026-09-xx xx:xx UTC)
+- Added labeled import buttons beside Add for your providers, profiles, chat templates, ERI v1 data sources, and document analysis policies. Paste an exported configuration snippet, review the filled form, and save it as your own item.
+- Added controls for organizations to allow or lock configuration snippet imports by item type. A locked Import button stays visible, and plugin archive imports keep their own controls.
+- Added a way to relink missing file attachments when you create a chat template from an exported configuration.
- Added a way to copy an entire chat, either with the button in the chat toolbar or next to the chat in the chat list. The copy opens right away so you can continue in it, while the original conversation stays exactly as it was. Many thanks to Peer Hogeterp (`peerschuett`) and Jens Erler (`j-erler`) for this feature.
- Added a way to roll a chat back to an earlier AI response. The response you pick stays, and every message after it is removed permanently, together with the attachments of those messages.
- Added a way to save a single code block of an answer. When an answer holds a web page, a LaTeX document, or a Markdown text, the export menu now offers that block as a file of its own.
diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md
index 186d59ed..c6ab12ae 100644
--- a/documentation/Enterprise IT.md
+++ b/documentation/Enterprise IT.md
@@ -619,6 +619,16 @@ Some exports ask a question first: a provider with an API key offers to include
Handing a whole plugin to a colleague is a different thing: that is the **Share** function on the plugins page, which writes a `.mwplugin` archive and is governed by its own organization setting rather than by the administration settings.
+### Importing one exported item for personal use
+
+Each parent screen has a button naming the item to import beside **Add** for profiles, LLM providers, embedding providers, transcription providers, chat templates, and document analysis policies. The data source screen has **Import ERI v1 Data Source** beside **Add Data Source**. Copy an exported snippet from the item's **Export configuration** control, paste one snippet of the matching type into the import dialog, and choose its item-specific import button. AI Studio reads the table as data; it does not run Lua code. The existing creation form then opens with its fields filled in. Review them and choose **Add** to create the item. Canceling either dialog leaves nothing saved. Tool Settings snippets cannot be imported this way.
+
+The imported item receives a new ID and the next local number. It belongs to you, even if the snippet came from an organization configuration. Existing provider, profile, tool, and data source references are kept; unavailable references are flagged so you can review them. Chat template attachments must point to existing local files. Relink any missing or relative paths in the creation dialog before saving.
+
+If an exported provider or ERI source contains an `ENC:v1` credential, AI Studio can fill it only on a device with the matching enterprise encryption secret. Otherwise, enter your own key, token, or password. A credential is stored in the local operating system secret store only when you save the item. Exporting still requires **Show administration settings**.
+
+All eight snippet import settings default to `true`. `DataApp.AllowUserToImportConfigurationSnippets` is the master switch; `DataApp.AllowUserToImportProfile`, `DataApp.AllowUserToImportLLMProvider`, `DataApp.AllowUserToImportEmbeddingProvider`, `DataApp.AllowUserToImportTranscriptionProvider`, `DataApp.AllowUserToImportChatTemplate`, `DataApp.AllowUserToImportERIDataSource`, and `DataApp.AllowUserToImportDocumentAnalysisPolicy` control the seven item types. The master and matching item setting must both allow the import. Provider imports also require `DataApp.AllowUserToAddProvider` and the matching provider Add permission. A blocked Import button stays visible with a lock. AI Studio checks these permissions when opening the import dialog and again before saving. Plugin archive import permissions are independent.
+
## Encrypted API Keys
You can include encrypted API keys in your configuration plugins for cloud providers (like OpenAI, Anthropic) or secured on-premise models. This feature provides obfuscation to prevent casual exposure of API keys in configuration files.