This commit is contained in:
Peer Hogeterp 2026-09-25 11:05:54 +00:00 committed by GitHub
commit 3b23a2682d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 2158 additions and 48 deletions

View File

@ -47,6 +47,7 @@ else
<MudButton OnClick="@this.AddPolicy" Disabled="@this.ArePolicyControlsDisabled" Variant="Variant.Filled" Color="Color.Primary">
@T("Add policy")
</MudButton>
<LockableButton Text="@T("Import document analysis policy")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))" Disabled="@(() => this.ArePolicyControlsDisabled)" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportPolicy" ButtonVariant="Variant.Outlined" NoMargin="true" />
<MudButton OnClick="@this.RemovePolicy" Disabled="@(this.ArePolicyControlsDisabled || (this.selectedPolicy?.IsProtected ?? true) || (this.selectedPolicy?.IsEnterpriseConfiguration ?? true))" Variant="Variant.Filled" Color="Color.Error">
@T("Delete this policy")
</MudButton>

View File

@ -9,6 +9,7 @@ using AIStudio.Settings.DataModel;
using AIStudio.Tools.AssistantSessions;
using Microsoft.AspNetCore.Components;
using LuaTable = Lua.LuaTable;
using SharedTools;
@ -483,6 +484,39 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return Task.CompletedTask;
}
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.AddImportedPolicy(table);
}
private async Task AddImportedPolicy(LuaTable importedConfiguration)
{
if (this.ArePolicyControlsDisabled || !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DOCUMENT_ANALYSIS_POLICIES"))
return;
var parameters = new DialogParameters<DocumentAnalysisPolicyDialog>
{
{ x => x.ImportedConfiguration, importedConfiguration },
};
var dialogReference = await this.DialogService.ShowAsync<DocumentAnalysisPolicyDialog>(T("Add policy"), parameters, DialogOptions.FULLSCREEN);
var result = await dialogReference.Result;
if (result is null || result.Canceled || result.Data is not DataDocumentAnalysisPolicy policy ||
!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 AddPolicy()
{
if (this.ArePolicyControlsDisabled)

View File

@ -1216,6 +1216,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"
@ -3925,6 +3928,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}"
@ -4885,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'."
@ -4960,6 +4969,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T146957
-- 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"
@ -5098,6 +5110,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."
@ -5746,6 +5761,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."
@ -5773,6 +5791,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro
-- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with."
-- Enter an absolute path to an existing local file, or remove this attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2603688456"] = "Enter an absolute path to an existing local file, or remove this attachment."
-- Predefined User Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input"
@ -5812,6 +5833,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, cha
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
-- Some attachments in this chat template are missing. Relink or remove them before adding the template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3342669429"] = "Some attachments in this chat template are missing. Relink or remove them before adding the template."
-- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
@ -5821,6 +5845,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
-- Relink the missing attachment '{0}' to an existing local file or remove it before saving.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3582611137"] = "Relink the missing attachment '{0}' to an existing local file or remove it before saving."
-- What system prompt do you want to use?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3652587353"] = "What system prompt do you want to use?"
@ -5860,6 +5887,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create
-- Enter a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
-- The chat template fields are malformed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T539301384"] = "The chat template fields are malformed."
-- Data Sources
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources"
@ -5869,6 +5899,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
-- Remove this attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment"
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
@ -5959,12 +5992,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"
-- {0} embedding provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} embedding provider"
-- 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?"
@ -6538,6 +6586,84 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T854110894
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T900713019"] = "Cancel"
-- No, the policy can be edited
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1286595725"] = "No, the policy can be edited"
-- 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."
-- Yes, protect this policy
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1762380857"] = "Yes, protect this policy"
-- 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"
-- No, show the policy definition
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3166091879"] = "No, show the policy definition"
-- 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"
-- Yes, hide the policy definition
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T940701960"] = "Yes, hide the policy definition"
-- 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."
@ -7879,6 +8005,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"
@ -8200,6 +8329,9 @@ 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."
@ -11590,6 +11722,114 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
-- The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T995107927"] = "The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used."
-- The embedded credential could not be decrypted on this device. Enter your own credential before saving.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1017166692"] = "The embedded credential could not be decrypted on this device. Enter your own credential before saving."
-- tool {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1561005241"] = "tool {0}"
-- provider {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1867642302"] = "provider {0}"
-- The '{0}' field must be a string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T224834942"] = "The '{0}' field must be a string."
-- The exported item has an invalid ID.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T2640740755"] = "The exported item has an invalid ID."
-- The '{0}' field has an unknown value.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T2930445640"] = "The '{0}' field has an unknown value."
-- data source {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3277968944"] = "data source {0}"
-- The '{0}' field must be a whole number.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3419687389"] = "The '{0}' field must be a whole number."
-- profile {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3445783292"] = "profile {0}"
-- The '{0}' field must be true or false.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3600915840"] = "The '{0}' field must be true or false."
-- Unavailable references: {0}. Review the selections before saving.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3637224236"] = "Unavailable references: {0}. Review the selections before saving."
-- The '{0}' field must contain an ENC:v1 credential.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T4261018324"] = "The '{0}' field must contain an ENC:v1 credential."
-- The '{0}' field must be a table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T432554775"] = "The '{0}' field must be a table."
-- The '{0}' field contains an invalid entry.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T948874113"] = "The '{0}' field contains an invalid entry."
-- The 'Port' field must be between 1 and 65535.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T1817689659"] = "The 'Port' field must be between 1 and 65535."
-- The 'MaxMatches' field is outside the allowed range.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T2009159518"] = "The 'MaxMatches' field is outside the allowed range."
-- This configuration section cannot be imported here.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T2657742500"] = "This configuration section cannot be imported here."
-- An example conversation message is empty.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T3131294453"] = "An example conversation message is empty."
-- An example conversation entry is not a table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T3912378391"] = "An example conversation entry is not a table."
-- Kerberos data sources cannot be imported from configuration snippets.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T4019138476"] = "Kerberos data sources cannot be imported from configuration snippets."
-- The chat template fields are malformed.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T539301384"] = "The chat template fields are malformed."
-- This data source is not an ERI v1 data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T724971220"] = "This data source is not an ERI v1 data source."
-- Only literal values are allowed at character {0}; executable Lua is not accepted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1200702179"] = "Only literal values are allowed at character {0}; executable Lua is not accepted."
-- Expected a quoted string at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}."
-- Unterminated long string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1583706986"] = "Unterminated long string."
-- The configuration section names do not match.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1700453124"] = "The configuration section names do not match."
-- The snippet contains too many nested tables.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1872841947"] = "The snippet contains too many nested tables."
-- Expected '{0}' at character {1}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2092509274"] = "Expected '{0}' at character {1}."
-- Unsupported string escape sequence: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2686740263"] = "Unsupported string escape sequence: {0}"
-- Paste one exported configuration snippet (up to 1 MB).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2743278967"] = "Paste one exported configuration snippet (up to 1 MB)."
-- Expected a comma or closing brace at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3466555352"] = "Expected a comma or closing brace at character {0}."
-- This is a {0} snippet. Paste a {1} snippet here.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3577683445"] = "This is a {0} snippet. Paste a {1} snippet here."
-- Unterminated quoted string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3882373828"] = "Unterminated quoted string."
-- A quoted string contains an unescaped newline.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T4168367785"] = "A quoted string contains an unescaped newline."
-- The field '{0}' occurs more than once.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T560836334"] = "The field '{0}' occurs more than once."
-- The snippet must contain exactly one table assignment and no executable code.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T873986754"] = "The snippet must contain exactly one table assignment and no executable code."
-- The table AUTHORS does not exist or is using an invalid syntax.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax."

View File

@ -105,14 +105,17 @@
</MudText>
}
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@T("Add Data Source")" Color="Color.Primary" Variant="Variant.Filled" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft" Class="mt-3 mb-6">
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.ERI_V1))">
@T("External Data (ERI-Server v1)")
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY))">
@T("Local Directory")
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_FILE))">
@T("Local File")
</MudMenuItem>
</MudMenu>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<MudMenu EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@T("Add Data Source")" Color="Color.Primary" Variant="Variant.Filled" AnchorOrigin="Origin.TopLeft" TransformOrigin="Origin.BottomLeft">
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.ERI_V1))">
@T("External Data (ERI-Server v1)")
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_DIRECTORY))">
@T("Local Directory")
</MudMenuItem>
<MudMenuItem OnClick="@(() => this.AddDataSource(DataSourceType.LOCAL_FILE))">
@T("Local File")
</MudMenuItem>
</MudMenu>
<LockableButton Text="@T("Import ERI v1 Data Source")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("DATA_SOURCES"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportERIDataSource" ButtonVariant="Variant.Outlined" NoMargin="true" />
</MudStack>

View File

@ -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<bool>(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<DataSourceERI_V1Dialog>(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!;

View File

@ -93,6 +93,9 @@
</MudText>
}
<LockableButton Text="@T("Add Embedding")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddEmbeddingProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddEmbeddingProvider" Class="mt-3" />
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<LockableButton Text="@T("Add Embedding")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddEmbeddingProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddEmbeddingProvider" Class="flex-grow-0" NoMargin="true" />
<LockableButton Text="@T("Import Embedding Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("EMBEDDING_PROVIDERS"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportEmbeddingProvider" ButtonVariant="Variant.Outlined" Class="flex-grow-0" NoMargin="true" />
</MudStack>
</ExpansionPanel>
}

View File

@ -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<EmbeddingProviderDialog>
{
{ x => x.IsEditing, false },
};
if (importedConfiguration is not null)
dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync<EmbeddingProviderDialog>(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!;

View File

@ -78,5 +78,8 @@
</MudText>
}
<LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddLLMProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="mt-3" />
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<LockableButton Text="@T("Add Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddLLMProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddLLMProvider" Class="flex-grow-0" NoMargin="true" />
<LockableButton Text="@T("Import LLM Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("LLM_PROVIDERS"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportProvider" ButtonVariant="Variant.Outlined" Class="flex-grow-0" NoMargin="true" />
</MudStack>
</ExpansionPanel>

View File

@ -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<ProviderDialog>
{
{ x => x.IsEditing, false },
};
if (importedConfiguration is not null)
dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync<ProviderDialog>(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!;

View File

@ -83,6 +83,9 @@
</MudText>
}
<LockableButton Text="@T("Add transcription provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddTranscriptionProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddTranscriptionProvider" Class="mt-3" />
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<LockableButton Text="@T("Add transcription provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.AllowUserToAddProvider || !this.SettingsManager.ConfigurationData.App.AllowUserToAddTranscriptionProvider)" Icon="@Icons.Material.Filled.AddRoad" OnClickAsync="@this.AddTranscriptionProvider" Class="flex-grow-0" NoMargin="true" />
<LockableButton Text="@T("Import Transcription Provider")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("TRANSCRIPTION_PROVIDERS"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportTranscriptionProvider" ButtonVariant="Variant.Outlined" Class="flex-grow-0" NoMargin="true" />
</MudStack>
</ExpansionPanel>
}

View File

@ -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<TranscriptionProviderDialog>
{
{ x => x.IsEditing, false },
};
if (importedConfiguration is not null)
dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync<TranscriptionProviderDialog>(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!;

View File

@ -4,6 +4,13 @@
<MudDialog>
<DialogContent>
@if (!this.IsEditing && !this.IsReadOnly)
{
@if (!string.IsNullOrWhiteSpace(this.importReferenceIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importReferenceIssue</MudAlert>
}
}
@* 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. *@
@ -94,6 +101,33 @@
<MudJustifiedText Class="mb-3" Typo="Typo.body1">
@T("You can attach files that will be automatically included when using this chat template. These files will be added to the first message sent in any chat using this template.")
</MudJustifiedText>
@if (!this.IsEditing && !this.IsReadOnly)
{
@if (this.HasUnresolvedAttachments)
{
<MudAlert Severity="Severity.Warning" Variant="Variant.Filled" Icon="@Icons.Material.Filled.WarningAmber" Class="mb-3">
@T("Some attachments in this chat template are missing. Relink or remove them before adding the template.")
</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(this.relinkIssue))
{
<MudAlert Severity="Severity.Error" Class="mb-2">@this.relinkIssue</MudAlert>
}
@for (var index = 0; index < this.attachmentsToRelink.Count; index++)
{
var currentIndex = index;
var attachment = this.attachmentsToRelink[index];
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-2">
<MudTextField T="string" Value="@attachment.ReplacementPath"
ValueChanged="@(value => this.UpdateRelinkPath(currentIndex, value))"
Label="@string.Format(T("Relink attachment: {0}"), attachment.OriginalPath)"
HelperText="@T("Enter an absolute path to an existing local file, or remove this attachment.")" Variant="Variant.Outlined" Immediate="@true" />
<MudTooltip Text="@T("Remove this attachment")">
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(() => this.RemoveAttachmentToRelink(currentIndex))" />
</MudTooltip>
</MudStack>
}
}
<AttachDocuments
Name="ChatTemplateFileAttachments"
@bind-DocumentPaths="@this.fileAttachments"
@ -252,7 +286,7 @@
@if (!this.isInlineEditOnGoing)
{
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary" Disabled="@this.HasUnresolvedAttachments">
@if (this.IsEditing)
{
@T("Update")
@ -265,4 +299,4 @@
}
}
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -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<ChatTemplateDialog> Logger { get; init; } = null!;
[Inject]
private ToolRegistry ToolRegistry { get; init; } = null!;
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
/// <summary>
@ -91,6 +101,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
private bool dataIsValid;
private List<ContentBlock> dataExampleConversation = [];
private HashSet<FileAttachment> fileAttachments = [];
private List<(string OriginalPath, string ReplacementPath)> attachmentsToRelink = [];
private string relinkIssue = string.Empty;
private bool HasUnresolvedAttachments => this.attachmentsToRelink.Any(attachment => !ConfigurationImportFields.IsExistingLocalFile(attachment.ReplacementPath));
private string importReferenceIssue = string.Empty;
private bool preselectTools;
private HashSet<string> selectedToolIds = new(StringComparer.Ordinal);
private bool preselectDataSources;
@ -143,6 +157,9 @@ public partial class ChatTemplateDialog : MSGComponentBase
this.DataName = this.ExistingChatThread.Name;
}
if (this.ImportedConfiguration is not null && this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
await this.ImportConfiguration(this.ImportedConfiguration);
await base.OnInitializedAsync();
}
@ -153,6 +170,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
if(!this.IsEditing && firstRender)
this.form.ResetValidation();
if (firstRender && this.ImportedConfiguration is not null &&
!this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))
this.MudDialog.Cancel();
await base.OnAfterRenderAsync(firstRender);
}
@ -176,6 +197,62 @@ public partial class ChatTemplateDialog : MSGComponentBase
IsEnterpriseConfiguration = false,
};
private async Task ImportConfiguration(LuaTable table)
{
ConfigurationSnippetImportValidation.Validate("CHAT_TEMPLATES", table);
if (!ChatTemplate.TryParseChatTemplateTable(0, table, Guid.Empty, string.Empty, out var parsed) || parsed is not ChatTemplate template)
throw new FormatException(T("The chat template fields are malformed."));
var paths = ConfigurationImportFields.Strings(table, "FileAttachments");
var validAttachments = new HashSet<FileAttachment>();
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();
}
private async Task<string> 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(ConfigurationImportFields.MissingDataSourceReference)
.Concat(missingTools.Select(ConfigurationImportFields.MissingToolReference)).ToList();
return ConfigurationImportFields.UnavailableReferencesIssue(missing);
}
private void UpdateRelinkPath(int index, string? path)
{
this.attachmentsToRelink[index] = (this.attachmentsToRelink[index].OriginalPath, path ?? string.Empty);
if (!this.HasUnresolvedAttachments)
this.relinkIssue = string.Empty;
}
private void RemoveAttachmentToRelink(int index)
{
this.attachmentsToRelink.RemoveAt(index);
if (!this.HasUnresolvedAttachments)
this.relinkIssue = string.Empty;
}
private void SetSelectedToolIds(HashSet<string> toolIds) => this.selectedToolIds = toolIds;
private void RemoveMessage(ContentBlock item)
@ -271,9 +348,24 @@ 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;
// Only check the relinked attachments here. They are added right before closing, so that a
// failed save does not leave a path behind which the user changes afterward:
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 or remove it before saving."), originalPath);
return;
}
}
await this.form.Validate();
// When the data is not valid, we don't store it:
@ -284,6 +376,10 @@ public partial class ChatTemplateDialog : MSGComponentBase
if (this.isInlineEditOnGoing)
return;
foreach (var (_, replacementPath) in this.attachmentsToRelink)
this.fileAttachments.Add(FileAttachment.FromPath(replacementPath));
this.attachmentsToRelink.Clear();
// Use the data model to store the chat template.
// We just return this data to the parent component:
var addedChatTemplateSettings = this.CreateChatTemplateSettings();
@ -329,4 +425,4 @@ public partial class ChatTemplateDialog : MSGComponentBase
}
private void Cancel() => this.MudDialog.Cancel();
}
}

View File

@ -0,0 +1,18 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
@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.")
</MudJustifiedText>
<MudTextField T="string" @bind-Text="@this.snippet" Immediate="true" Label="@T("Configuration snippet")" Lines="6" AutoGrow="true" MaxLines="14" Variant="Variant.Outlined" />
@if (!string.IsNullOrWhiteSpace(this.issue))
{
<MudAlert Severity="Severity.Error" Class="mt-3">@this.issue</MudAlert>
}
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel">@T("Cancel")</MudButton>
<MudButton OnClick="@this.Import" Disabled="@string.IsNullOrWhiteSpace(this.snippet)" Color="Color.Primary" Variant="Variant.Filled">@this.ImportLabel</MudButton>
</DialogActions>
</MudDialog>

View File

@ -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<LuaTable?> ShowAsync(IDialogService service, string section, string title)
{
var parameters = new DialogParameters<ConfigurationSnippetImportDialog>
{
{ x => x.Section, section },
{ x => x.ImportLabel, title },
};
var dialog = await service.ShowAsync<ConfigurationSnippetImportDialog>(title, parameters, DialogOptions.FULLSCREEN);
var result = await dialog.Result;
return result is { Canceled: false, Data: LuaTable table } ? table : null;
}
}

View File

@ -5,6 +5,17 @@
<MudDialog>
<DialogContent>
@if (!this.IsEditing)
{
@if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importCredentialIssue</MudAlert>
}
@if (!string.IsNullOrWhiteSpace(this.importRetrievalIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importRetrievalIssue</MudAlert>
}
}
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
@* ReSharper disable once CSharpWarnings::CS8974 *@
<MudTextField
@ -102,7 +113,7 @@
@if (this.availableRetrievalProcesses.Count > 0)
{
<MudSelect @bind-Value="@this.dataSelectedRetrievalProcess" Text="@this.dataSelectedRetrievalProcess.Name" Label="@T("Select one retrieval process")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateRetrievalProcess">
<MudSelect @bind-Value="@this.dataSelectedRetrievalProcess" @bind-Value:after="@this.ClearImportRetrievalIssue" Text="@this.dataSelectedRetrievalProcess.Name" Label="@T("Select one retrieval process")" Class="mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start" Validation="@this.dataSourceValidation.ValidateRetrievalProcess">
@foreach (var retrievalProcess in this.availableRetrievalProcesses)
{
<MudSelectItem Value="@retrievalProcess">
@ -141,4 +152,4 @@
}
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -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,9 @@ 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 importedRetrievalId = string.Empty;
private string dataEditingPreviousInstanceName = string.Empty;
private List<AuthMethod> availableAuthMethods = [];
private DataSourceSecurity dataSecurityPolicy;
@ -143,6 +152,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,7 +194,49 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
MaxMatches = this.dataMaxMatches,
};
}
private Task ImportConfiguration(LuaTable table)
{
ConfigurationSnippetImportValidation.Validate("DATA_SOURCES", table);
var name = ConfigurationImportFields.String(table, "Name");
var hostname = ConfigurationImportFields.String(table, "Hostname");
var port = ConfigurationImportFields.Int(table, "Port");
var authMethod = ConfigurationImportFields.Enum<AuthMethod>(table, "AuthMethod");
var securityPolicy = ConfigurationImportFields.Enum<DataSourceSecurity>(table, "SecurityPolicy");
var retrievalId = ConfigurationImportFields.String(table, "SelectedRetrievalId");
var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
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.importedRetrievalId = retrievalId;
this.connectionTested = false;
this.connectionSuccessfulTested = false;
this.form.ResetValidation();
return Task.CompletedTask;
}
private void ClearImportRetrievalIssue() => this.importRetrievalIssue = string.Empty;
private bool IsConnectionEncrypted() => this.dataHostname.StartsWith("https://", StringComparison.InvariantCultureIgnoreCase);
private bool IsConnectionPossible()
@ -251,6 +311,24 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
}
this.availableRetrievalProcesses = retrievalInfoRequest.Data ?? [];
// Only the first successful test after an import resolves the imported retrieval ID;
// afterward, the selection belongs to the user:
if (!string.IsNullOrWhiteSpace(this.importedRetrievalId))
{
var importedRetrieval = this.availableRetrievalProcesses.FirstOrDefault(item => item.Id == this.importedRetrievalId);
if (importedRetrieval != default)
{
this.dataSelectedRetrievalProcess = importedRetrieval;
this.importRetrievalIssue = string.Empty;
}
else
{
this.importRetrievalIssue = string.Format(T("The imported retrieval process '{0}' is unavailable. Select another process before saving."), this.importedRetrievalId);
this.dataSelectedRetrievalProcess = default;
}
this.importedRetrievalId = string.Empty;
}
this.connectionTested = true;
this.connectionSuccessfulTested = true;
@ -304,6 +382,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 +418,4 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId
}
private void Cancel() => this.MudDialog.Cancel();
}
}

View File

@ -0,0 +1,45 @@
@using AIStudio.Provider
@using AIStudio.Settings
@using AIStudio.Settings.DataModel
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
@if (!string.IsNullOrWhiteSpace(this.referenceIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.referenceIssue</MudAlert>
}
<MudForm @ref="@this.form" @bind-IsValid="@this.isValid" @bind-Errors="@this.issues">
<MudTextField T="string" @bind-Text="@this.name" Validation="@this.ValidateName" Immediate="true" Label="@T("Policy name")" Counter="60" MaxLength="60" Variant="Variant.Outlined" Class="mb-3" />
<MudTextField T="string" @bind-Text="@this.description" Validation="@this.ValidateDescription" Immediate="true" Label="@T("Policy description")" Counter="512" MaxLength="512" Lines="3" AutoGrow="true" Variant="Variant.Outlined" Class="mb-3" />
<MudTextSwitch @bind-Value="@this.hideDefinition" Label="@T("Hide the policy definition when distributed via configuration plugin?")" LabelOn="@T("Yes, hide the policy definition")" LabelOff="@T("No, show the policy definition")" />
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.minimumConfidence)" SelectionUpdate="@(value => this.minimumConfidence = value)" />
<ToolSelectionField Component="@AIStudio.Tools.Components.DOCUMENT_ANALYSIS_ASSISTANT" SelectedToolIds="@this.allowedToolIds" SelectedToolIdsChanged="@(value => this.allowedToolIds = value)" Label="@T("Tools this policy permits")" />
<MudSelect T="string" @bind-Value="@this.providerId" Label="@T("Preselect a provider")" Variant="Variant.Outlined" Class="mb-3">
<MudSelectItem T="string" Value="@string.Empty">@T("No provider")</MudSelectItem>
@foreach (var provider in this.SettingsManager.GetAllProviders())
{
<MudSelectItem T="string" Value="@provider.Id">@provider.InstanceName</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" @bind-Value="@this.profileId" Label="@T("Preselect a profile")" Variant="Variant.Outlined" Class="mb-3">
<MudSelectItem T="string" Value="@string.Empty">@T("Use app default profile")</MudSelectItem>
<MudSelectItem T="string" Value="@Profile.NO_PROFILE.Id">@T("No profile")</MudSelectItem>
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudSelectItem T="string" Value="@profile.Id">@profile.Name</MudSelectItem>
}
</MudSelect>
<MudTextSwitch @bind-Value="@this.isProtected" Label="@T("Would you like to protect this policy so that you cannot accidentally edit or delete it?")" LabelOn="@T("Yes, protect this policy")" LabelOff="@T("No, the policy can be edited")" />
<MudTextField T="string" @bind-Text="@this.analysisRules" Validation="@this.ValidateAnalysisRules" Immediate="true" Label="@T("Analysis rules")" Lines="5" AutoGrow="true" MaxLines="26" Variant="Variant.Outlined" Class="mb-3" />
<ReadFileContent Text="@T("Load analysis rules from document")" @bind-FileContent="@this.analysisRules" EnableDragDrop="true" />
<MudTextField T="string" @bind-Text="@this.outputRules" Validation="@this.ValidateOutputRules" Immediate="true" Label="@T("Output rules")" Lines="5" AutoGrow="true" MaxLines="26" Variant="Variant.Outlined" Class="mb-3" />
<ReadFileContent Text="@T("Load output rules from document")" @bind-FileContent="@this.outputRules" EnableDragDrop="true" />
</MudForm>
<Issues IssuesData="@this.issues" />
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Cancel" Variant="Variant.Filled">@T("Cancel")</MudButton>
<MudButton OnClick="@this.Store" Variant="Variant.Filled" Color="Color.Primary">@T("Add")</MudButton>
</DialogActions>
</MudDialog>

View File

@ -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<string> 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<ConfidenceLevel>(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<string>();
if (!string.IsNullOrWhiteSpace(this.providerId) && this.SettingsManager.GetAllProviders().All(provider => provider.Id != this.providerId))
missing.Add(ConfigurationImportFields.MissingProviderReference(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(ConfigurationImportFields.MissingProfileReference(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(ConfigurationImportFields.MissingToolReference));
this.referenceIssue = ConfigurationImportFields.UnavailableReferencesIssue(missing);
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();
}

View File

@ -5,6 +5,13 @@
<MudDialog>
<DialogContent>
@if (!this.IsEditing)
{
@if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importCredentialIssue</MudAlert>
}
}
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">

View File

@ -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<LLMProviders>(table, "UsedLLMProvider");
var host = ConfigurationImportFields.Enum<Host>(table, "Host");
var hostname = ConfigurationImportFields.String(table, "Hostname");
var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
? ConfigurationImportFields.Enum<HFInferenceProvider>(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();

View File

@ -99,4 +99,4 @@
</MudButton>
}
</DialogActions>
</MudDialog>
</MudDialog>

View File

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

View File

@ -4,6 +4,13 @@
@inherits MSGComponentBase
<MudDialog>
<DialogContent>
@if (!this.IsEditing)
{
@if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importCredentialIssue</MudAlert>
}
}
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">

View File

@ -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;
/// </summary>
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<LLMProviders>(table, "UsedLLMProvider");
var host = ConfigurationImportFields.Enum<Host>(table, "Host");
var hostname = ConfigurationImportFields.String(table, "Hostname");
var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
? ConfigurationImportFields.Enum<HFInferenceProvider>(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();

View File

@ -81,13 +81,16 @@
</MudText>
}
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddChatTemplate">
@T("Add Chat Template")
</MudButton>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" OnClick="@this.AddChatTemplate">
@T("Add Chat Template")
</MudButton>
<LockableButton Text="@T("Import Chat Template")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("CHAT_TEMPLATES"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportChatTemplate" ButtonVariant="Variant.Outlined" NoMargin="true" />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

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

View File

@ -64,13 +64,16 @@
</MudText>
}
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" Class="mt-3 mb-6" OnClick="@this.AddProfile">
@T("Add Profile")
</MudButton>
<MudStack Row="true" Wrap="Wrap.Wrap" Class="mt-3 mb-6">
<MudButton Variant="Variant.Filled" Color="@Color.Primary" StartIcon="@Icons.Material.Filled.AddRoad" OnClick="@this.AddProfile">
@T("Add Profile")
</MudButton>
<LockableButton Text="@T("Import Profile")" IsLocked="@(() => !this.SettingsManager.ConfigurationData.App.CanImportConfigurationSnippet("PROFILES"))" Icon="@Icons.Material.Filled.FileDownload" OnClickAsync="@this.ImportProfile" ButtonVariant="Variant.Outlined" NoMargin="true" />
</MudStack>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">
@T("Close")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -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<ProfileDialog>
{
{ x => x.IsEditing, false },
};
if (importedConfiguration is not null)
dialogParameters.Add(x => x.ImportedConfiguration, importedConfiguration);
var dialogReference = await this.DialogService.ShowAsync<ProfileDialog>(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<bool>(this, Event.CONFIGURATION_CHANGED);
}
}
}

View File

@ -5,6 +5,13 @@
<MudDialog>
<DialogContent>
@if (!this.IsEditing)
{
@if (!string.IsNullOrWhiteSpace(this.importCredentialIssue))
{
<MudAlert Severity="Severity.Warning" Class="mb-2">@this.importCredentialIssue</MudAlert>
}
}
@if (this.IsEnterpriseConfiguration)
{
<MudAlert Severity="Severity.Info" Class="mb-4">

View File

@ -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<LLMProviders>(table, "UsedLLMProvider");
var host = ConfigurationImportFields.Enum<Host>(table, "Host");
var hostname = ConfigurationImportFields.String(table, "Hostname");
var hfProvider = table.TryGetValue("HFInferenceProvider", out _)
? ConfigurationImportFields.Enum<HFInferenceProvider>(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;

View File

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

View File

@ -1218,6 +1218,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"
@ -3927,6 +3930,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}"
@ -4887,6 +4893,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'."
@ -4962,6 +4971,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELPROVIDERS::T146957
-- 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"
@ -5100,6 +5112,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."
@ -5748,6 +5763,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."
@ -5775,6 +5793,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579080722"] = "No, pro
-- You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2579208700"] = "You might want to predefine a first message that will be copied into the user prompt, when you use this chat template. This message could for example be a blueprint for a structured message that this chat template is defined to work with."
-- Enter an absolute path to an existing local file, or remove this attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2603688456"] = "Enter an absolute path to an existing local file, or remove this attachment."
-- Predefined User Input
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T2607897066"] = "Predefined User Input"
@ -5814,6 +5835,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3268470871"] = "No, cha
-- A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3329415439"] = "A chat template may decide which tools a chat starts with. Without such a decision, those chats start with the tools you chose as your default in the chat options. Deciding and then picking nothing is a different statement: such chats start with no tool at all, no matter what your default says."
-- Some attachments in this chat template are missing. Relink or remove them before adding the template.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3342669429"] = "Some attachments in this chat template are missing. Relink or remove them before adding the template."
-- Add a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3372872324"] = "Add a message"
@ -5823,6 +5847,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3448155331"] = "Close"
-- Unsupported content type
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3570316759"] = "Unsupported content type"
-- Relink the missing attachment '{0}' to an existing local file or remove it before saving.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3582611137"] = "Relink the missing attachment '{0}' to an existing local file or remove it before saving."
-- What system prompt do you want to use?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T3652587353"] = "What system prompt do you want to use?"
@ -5862,6 +5889,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T4199560726"] = "Create
-- Enter a message
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T446374405"] = "Enter a message"
-- The chat template fields are malformed.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T539301384"] = "The chat template fields are malformed."
-- Data Sources
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T558345131"] = "Data Sources"
@ -5871,6 +5901,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T628396066"] = "System P
-- A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T650711085"] = "A chat started with this template begins with these data sources and options. All of it stays changeable in the chat itself."
-- Remove this attachment
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T70791272"] = "Remove this attachment"
-- The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CHATTEMPLATEDIALOG::T722788866"] = "The selection stays changeable in the chat, and every tool still has to meet the confidence requirements of the provider in use."
@ -5961,12 +5994,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T767586087"
-- {0} embedding provider
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::CONFIGURATIONPLUGINDELETEDIALOG::T818101181"] = "{0} embedding provider"
-- 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?"
@ -6540,6 +6588,84 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T854110894
-- Cancel
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DIRECTCHATLAUNCHERSETTINGSDIALOG::T900713019"] = "Cancel"
-- No, the policy can be edited
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1286595725"] = "No, the policy can be edited"
-- 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."
-- Yes, protect this policy
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T1762380857"] = "Yes, protect this policy"
-- 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"
-- No, show the policy definition
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T3166091879"] = "No, show the policy definition"
-- 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"
-- Yes, hide the policy definition
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DOCUMENTANALYSISPOLICYDIALOG::T940701960"] = "Yes, hide the policy definition"
-- 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."
@ -7881,6 +8007,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"
@ -8202,6 +8331,9 @@ 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."
@ -11592,6 +11724,114 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECUR
-- The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTSECURITYRESOLVER::T995107927"] = "The plugin code changed after the last security audit. The stored result no longer matches the current code, so this assistant plugin must be audited again before it may be enabled or used."
-- The embedded credential could not be decrypted on this device. Enter your own credential before saving.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1017166692"] = "The embedded credential could not be decrypted on this device. Enter your own credential before saving."
-- tool {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1561005241"] = "tool {0}"
-- provider {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T1867642302"] = "provider {0}"
-- The '{0}' field must be a string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T224834942"] = "The '{0}' field must be a string."
-- The exported item has an invalid ID.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T2640740755"] = "The exported item has an invalid ID."
-- The '{0}' field has an unknown value.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T2930445640"] = "The '{0}' field has an unknown value."
-- data source {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3277968944"] = "data source {0}"
-- The '{0}' field must be a whole number.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3419687389"] = "The '{0}' field must be a whole number."
-- profile {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3445783292"] = "profile {0}"
-- The '{0}' field must be true or false.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3600915840"] = "The '{0}' field must be true or false."
-- Unavailable references: {0}. Review the selections before saving.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T3637224236"] = "Unavailable references: {0}. Review the selections before saving."
-- The '{0}' field must contain an ENC:v1 credential.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T4261018324"] = "The '{0}' field must contain an ENC:v1 credential."
-- The '{0}' field must be a table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T432554775"] = "The '{0}' field must be a table."
-- The '{0}' field contains an invalid entry.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONIMPORTFIELDS::T948874113"] = "The '{0}' field contains an invalid entry."
-- The 'Port' field must be between 1 and 65535.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T1817689659"] = "The 'Port' field must be between 1 and 65535."
-- The 'MaxMatches' field is outside the allowed range.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T2009159518"] = "The 'MaxMatches' field is outside the allowed range."
-- This configuration section cannot be imported here.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T2657742500"] = "This configuration section cannot be imported here."
-- An example conversation message is empty.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T3131294453"] = "An example conversation message is empty."
-- An example conversation entry is not a table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T3912378391"] = "An example conversation entry is not a table."
-- Kerberos data sources cannot be imported from configuration snippets.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T4019138476"] = "Kerberos data sources cannot be imported from configuration snippets."
-- The chat template fields are malformed.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T539301384"] = "The chat template fields are malformed."
-- This data source is not an ERI v1 data source.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETIMPORTVALIDATION::T724971220"] = "This data source is not an ERI v1 data source."
-- Only literal values are allowed at character {0}; executable Lua is not accepted.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1200702179"] = "Only literal values are allowed at character {0}; executable Lua is not accepted."
-- Expected a quoted string at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1235695745"] = "Expected a quoted string at character {0}."
-- Unterminated long string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1583706986"] = "Unterminated long string."
-- The configuration section names do not match.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1700453124"] = "The configuration section names do not match."
-- The snippet contains too many nested tables.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T1872841947"] = "The snippet contains too many nested tables."
-- Expected '{0}' at character {1}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2092509274"] = "Expected '{0}' at character {1}."
-- Unsupported string escape sequence: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2686740263"] = "Unsupported string escape sequence: {0}"
-- Paste one exported configuration snippet (up to 1 MB).
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T2743278967"] = "Paste one exported configuration snippet (up to 1 MB)."
-- Expected a comma or closing brace at character {0}.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3466555352"] = "Expected a comma or closing brace at character {0}."
-- This is a {0} snippet. Paste a {1} snippet here.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3577683445"] = "This is a {0} snippet. Paste a {1} snippet here."
-- Unterminated quoted string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T3882373828"] = "Unterminated quoted string."
-- A quoted string contains an unescaped newline.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T4168367785"] = "A quoted string contains an unescaped newline."
-- The field '{0}' occurs more than once.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T560836334"] = "The field '{0}' occurs more than once."
-- The snippet must contain exactly one table assignment and no executable code.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::CONFIGURATIONSNIPPETPARSER::T873986754"] = "The snippet must contain exactly one table assignment and no executable code."
-- The table AUTHORS does not exist or is using an invalid syntax.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINBASE::T1068328139"] = "The table AUTHORS does not exist or is using an invalid syntax."

View File

@ -181,6 +181,29 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
/// </summary>
public bool AllowUserToAddTranscriptionProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.AllowUserToAddTranscriptionProvider, true);
/// <summary>Should the user be allowed to import exported configuration snippets?</summary>
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,
});
/// <summary>
/// Should the user be allowed to import plugin archives from disk?
/// </summary>

View File

@ -256,7 +256,7 @@ public sealed record EmbeddingProvider(
["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}",
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
["TokenizerPath"] = "{{this.TokenizerPath}}",
["TokenizerPath"] = "{{LuaTools.EscapeLuaString(this.TokenizerPath)}}",
["TokenLimit"] = {{this.EffectiveTokenLimit}},
["EmbeddingBatchSize"] = {{this.EffectiveEmbeddingBatchSize}},

View File

@ -279,7 +279,7 @@ public sealed record Provider(
["InstanceName"] = "{{LuaTools.EscapeLuaString(this.InstanceName)}}",
["UsedLLMProvider"] = "{{this.UsedLLMProvider}}",
["TokenizerPath"] = "{{this.TokenizerPath}}",
["TokenizerPath"] = "{{LuaTools.EscapeLuaString(this.TokenizerPath)}}",
["Host"] = "{{this.Host}}",
["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}",

View File

@ -0,0 +1,105 @@
using AIStudio.Tools;
using Lua;
namespace AIStudio.Tools.PluginSystem;
public static class ConfigurationImportFields
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationImportFields).Namespace, nameof(ConfigurationImportFields));
public static void ValidateExportId(LuaTable table)
{
if (!Guid.TryParse(String(table, "Id"), out _))
throw new FormatException(TB("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<string>(out var text))
return text;
throw new FormatException(string.Format(TB("The '{0}' field must be a string."), name));
}
if (!required)
return string.Empty;
throw new FormatException(string.Format(TB("The '{0}' field must be a string."), name));
}
public static LuaTable Table(LuaTable table, string name)
{
if (table.TryGetValue(name, out var value) && value.Type is LuaValueType.Table && value.TryRead<LuaTable>(out var nested))
return nested;
throw new FormatException(string.Format(TB("The '{0}' field must be a table."), name));
}
public static T Enum<T>(LuaTable table, string name) where T : struct, Enum
{
var text = String(table, name);
if (System.Enum.TryParse<T>(text, true, out var result) && System.Enum.IsDefined(result))
return result;
throw new FormatException(string.Format(TB("The '{0}' field has an unknown value."), name));
}
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<bool>(out var result))
return result;
throw new FormatException(string.Format(TB("The '{0}' field must be true or false."), name));
}
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<double>(out var number) && number >= int.MinValue && number <= int.MaxValue && number == Math.Truncate(number))
return (int)number;
throw new FormatException(string.Format(TB("The '{0}' field must be a whole number."), name));
}
public static List<string> Strings(LuaTable table, string name)
{
var nested = Table(table, name);
var result = new List<string>();
for (var i = 1; i <= nested.ArrayLength; i++)
{
if (nested[i].Type is not LuaValueType.String || !nested[i].TryRead<string>(out var text) || string.IsNullOrWhiteSpace(text))
throw new FormatException(string.Format(TB("The '{0}' field contains an invalid entry."), name));
result.Add(text);
}
return result;
}
public static bool IsExistingLocalFile(string path) => Path.IsPathFullyQualified(path) && File.Exists(path);
/// <summary>Lists the given references as a warning, or returns an empty text when nothing is missing.</summary>
public static string UnavailableReferencesIssue(IReadOnlyCollection<string> missing) => missing.Count == 0
? string.Empty
: string.Format(TB("Unavailable references: {0}. Review the selections before saving."), string.Join(", ", missing));
public static string MissingProviderReference(string id) => string.Format(TB("provider {0}"), id);
public static string MissingProfileReference(string id) => string.Format(TB("profile {0}"), id);
public static string MissingToolReference(string id) => string.Format(TB("tool {0}"), id);
public static string MissingDataSourceReference(string id) => string.Format(TB("data source {0}"), id);
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(string.Format(TB("The '{0}' field must contain an ENC:v1 credential."), name));
var encryption = decryptionService ?? PluginFactory.EnterpriseEncryption;
if (encryption?.IsAvailable == true && encryption.TryDecrypt(encrypted, out var decrypted))
return decrypted;
issue = TB("The embedded credential could not be decrypted on this device. Enter your own credential before saving.");
return string.Empty;
}
}

View File

@ -0,0 +1,137 @@
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;
/// <summary>Checks the fields used by the creation forms before leaving the paste dialog.</summary>
public static class ConfigurationSnippetImportValidation
{
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationSnippetImportValidation).Namespace, nameof(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<ConfidenceLevel>(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(TB("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<LLMProviders>(table, "UsedLLMProvider");
ConfigurationImportFields.Enum<Host>(table, "Host");
ConfigurationImportFields.String(table, "Hostname");
if (table.TryGetValue("HFInferenceProvider", out _))
ConfigurationImportFields.Enum<HFInferenceProvider>(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<LuaTable>(out var message))
throw new FormatException(TB("An example conversation entry is not a table."));
ConfigurationImportFields.Enum<ChatRole>(message, "Role");
if (string.IsNullOrWhiteSpace(ConfigurationImportFields.String(message, "Content")))
throw new FormatException(TB("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(TB("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(TB("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(TB("The 'Port' field must be between 1 and 65535."));
var authMethod = ConfigurationImportFields.Enum<AuthMethod>(table, "AuthMethod");
if (authMethod is AuthMethod.KERBEROS)
throw new FormatException(TB("Kerberos data sources cannot be imported from configuration snippets."));
ConfigurationImportFields.Enum<DataSourceSecurity>(table, "SecurityPolicy");
ConfigurationImportFields.String(table, "SelectedRetrievalId");
var maxMatches = ConfigurationImportFields.Int(table, "MaxMatches", 10);
if (maxMatches is < 1 or > ushort.MaxValue)
throw new FormatException(TB("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);
}
}

View File

@ -0,0 +1,243 @@
using System.Globalization;
using System.Text;
using Lua;
namespace AIStudio.Tools.PluginSystem;
/// <summary>Reads one exported configuration assignment as data. No Lua state is created.</summary>
public sealed class ConfigurationSnippetParser
{
private readonly string source;
private int position;
private ConfigurationSnippetParser(string source) => this.source = source;
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfigurationSnippetParser).Namespace, nameof(ConfigurationSnippetParser));
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 = TB("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(string.Format(TB("This is a {0} snippet. Paste a {1} snippet here."), section, expectedSection));
parser.Expect('[');
parser.Expect('#');
parser.ExpectWord("CONFIG");
if (parser.ReadBracketedString() != expectedSection)
throw new FormatException(TB("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(TB("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(TB("The snippet contains too many nested tables."));
this.Expect('{');
var table = new LuaTable();
var arrayIndex = 1;
var names = new HashSet<string>(StringComparer.Ordinal);
while (true)
{
this.SkipTrivia();
if (this.Take('}'))
return table;
// "[[" or "[=" opens a long string, which is an array value rather than a bracketed key:
if (this.Peek() == '[' && !this.IsLongStringStart())
{
this.position++;
var key = this.ReadString();
this.Expect(']');
this.Expect('=');
if (!names.Add(key))
throw new FormatException(string.Format(TB("The field '{0}' occurs more than once."), key));
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(string.Format(TB("Expected a comma or closing brace at character {0}."), 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(string.Format(TB("Only literal values are allowed at character {0}; executable Lua is not accepted."), start + 1));
}
private bool IsLongStringStart() => this.position + 1 < this.source.Length && this.source[this.position + 1] is '[' or '=';
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];
if (this.Peek() != '[')
throw new FormatException(string.Format(TB("Expected '{0}' at character {1}."), '[', this.position + 1));
this.position++;
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(TB("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(string.Format(TB("Expected a quoted string at character {0}."), 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(TB("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(string.Format(TB("Unsupported string escape sequence: {0}"), "\\" + c)),
});
}
throw new FormatException(TB("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(string.Format(TB("Expected '{0}' at character {1}."), c, 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(string.Format(TB("Expected '{0}' at character {1}."), word, this.position + 1));
}
}

View File

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

View File

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

View File

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