Multiple Profiles can now be selected

This commit is contained in:
Peer Hogeterp 2026-09-07 16:09:00 +02:00
parent d043fbc8f0
commit cefc5464d5
89 changed files with 1360 additions and 503 deletions

View File

@ -172,7 +172,7 @@
@if (this.AllowProfiles && this.ShowProfileSelection)
{
<ProfileSelection MarginLeft="" @bind-CurrentProfile="@this.CurrentProfile"/>
<ProfileSelection MarginLeft="" @bind-SelectedProfileIds="@this.CurrentProfileIds"/>
}
@* No selection where the assistant's own rules already name the tools: *@

View File

@ -189,7 +189,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
this.MightPreselectValues();
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.CurrentProfileIds = this.SettingsManager.GetPreselectedProfiles(this.Component).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
this.SelectedToolIds = this.SettingsManager.GetDefaultToolIds(this.Component);
await this.OnDefaultsAppliedAsync();
@ -362,7 +362,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
{
IncludeDateTime = false,
SelectedProvider = this.ProviderSettings.Id,
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
SelectedProfileIds = this.AllowProfiles ? [..this.CurrentProfileIds] : [],
SystemPrompt = this.SystemPrompt,
WorkspaceId = Guid.Empty,
ChatId = Guid.NewGuid(),
@ -379,7 +379,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
{
IncludeDateTime = false,
SelectedProvider = this.ProviderSettings.Id,
SelectedProfile = this.AllowProfiles ? this.CurrentProfile.Id : Profile.NO_PROFILE.Id,
SelectedProfileIds = this.AllowProfiles ? [..this.CurrentProfileIds] : [],
SystemPrompt = this.SystemPrompt,
WorkspaceId = workspaceId,
ChatId = chatId,
@ -400,7 +400,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
protected virtual void ResetProviderAndProfileSelection()
{
this.ProviderSettings = this.SettingsManager.GetPreselectedProvider(this.Component);
this.CurrentProfile = this.SettingsManager.GetPreselectedProfile(this.Component);
this.CurrentProfileIds = this.SettingsManager.GetPreselectedProfiles(this.Component).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.CurrentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(this.Component);
this.SelectedToolIds = this.SettingsManager.GetDefaultToolIds(this.Component);
}
@ -975,7 +975,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
var state = new AssistantSessionStateWriter();
state.Set(PROVIDER_SETTINGS_STATE_KEY, this.ProviderSettings);
state.Set(INPUT_IS_VALID_STATE_KEY, this.InputIsValid);
state.Set(CURRENT_PROFILE_STATE_KEY, this.CurrentProfile);
state.Set(CURRENT_PROFILE_IDS_STATE_KEY, this.CurrentProfileIds);
state.Set(CURRENT_CHAT_TEMPLATE_STATE_KEY, this.CurrentChatTemplate);
state.Set(CHAT_THREAD_STATE_KEY, this.ChatThread);
state.Set(LAST_USER_PROMPT_STATE_KEY, this.LastUserPrompt);
@ -1003,7 +1003,7 @@ public abstract partial class AssistantBase<TSettings> : AssistantLowerBase wher
var reader = new AssistantSessionStateReader(state, this.Title);
reader.Restore(PROVIDER_SETTINGS_STATE_KEY, value => this.ProviderSettings = value);
reader.Restore(INPUT_IS_VALID_STATE_KEY, value => this.InputIsValid = value);
reader.Restore(CURRENT_PROFILE_STATE_KEY, value => this.CurrentProfile = value);
reader.Restore(CURRENT_PROFILE_IDS_STATE_KEY, value => this.CurrentProfileIds = value);
reader.Restore(CURRENT_CHAT_TEMPLATE_STATE_KEY, value => this.CurrentChatTemplate = value);
reader.Restore(CHAT_THREAD_STATE_KEY, value => this.ChatThread = value);
reader.Restore(LAST_USER_PROMPT_STATE_KEY, value => this.LastUserPrompt = value);

View File

@ -15,7 +15,7 @@ public abstract class AssistantLowerBase : MSGComponentBase
protected static readonly AssistantSessionStateKey<AIStudio.Settings.Provider> PROVIDER_SETTINGS_STATE_KEY = new(nameof(ProviderSettings));
protected static readonly AssistantSessionStateKey<bool> INPUT_IS_VALID_STATE_KEY = new(nameof(InputIsValid));
protected static readonly AssistantSessionStateKey<Profile> CURRENT_PROFILE_STATE_KEY = new(nameof(CurrentProfile));
protected static readonly AssistantSessionStateKey<HashSet<string>> CURRENT_PROFILE_IDS_STATE_KEY = new(nameof(CurrentProfileIds));
protected static readonly AssistantSessionStateKey<ChatTemplate> CURRENT_CHAT_TEMPLATE_STATE_KEY = new(nameof(CurrentChatTemplate));
protected static readonly AssistantSessionStateKey<ChatThread?> CHAT_THREAD_STATE_KEY = new(nameof(ChatThread));
protected static readonly AssistantSessionStateKey<IContent?> LAST_USER_PROMPT_STATE_KEY = new(nameof(LastUserPrompt));
@ -26,7 +26,7 @@ public abstract class AssistantLowerBase : MSGComponentBase
protected AIStudio.Settings.Provider ProviderSettings = Settings.Provider.NONE;
protected bool InputIsValid;
protected Profile CurrentProfile = Profile.NO_PROFILE;
protected HashSet<string> CurrentProfileIds = [];
protected ChatTemplate CurrentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
protected ChatThread? ChatThread;
protected IContent? LastUserPrompt;

View File

@ -102,7 +102,7 @@ public partial class AssistantBatchProcessing
{
IncludeDateTime = false,
SelectedProvider = this.ProviderSettings.Id,
SelectedProfile = Profile.NO_PROFILE.Id,
SelectedProfileIds = [],
SelectedToolIds = [..this.SelectedToolIds],
SystemPrompt = this.SystemPrompt,
WorkspaceId = Guid.Empty,

View File

@ -36,7 +36,7 @@
<DirectChatLauncherForm WorkspaceName="@this.launcherWorkspaceName"
WorkspaceNameChanged="@this.LauncherWorkspaceNameChanged"
@bind-ProviderId="@this.launcherProviderId"
@bind-ProfileId="@this.launcherProfileId"
@bind-ProfileIds="@this.launcherProfileIds"
@bind-ChatTemplateId="@this.launcherChatTemplateId"
@bind-DataSourceIds="@this.launcherDataSourceIds"
@bind-ToolIds="@this.launcherToolIds"

View File

@ -99,7 +99,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
private string descriptionSuggestion = string.Empty;
private string launcherWorkspaceName = string.Empty;
private string launcherProviderId = string.Empty;
private string launcherProfileId = string.Empty;
private HashSet<string>? launcherProfileIds;
private string launcherChatTemplateId = string.Empty;
private IEnumerable<string> launcherDataSourceIds = [];
private HashSet<string> launcherToolIds = [];
@ -137,7 +137,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
private static readonly AssistantSessionStateKey<string> DESCRIPTION_SUGGESTION_STATE_KEY = new(nameof(descriptionSuggestion));
private static readonly AssistantSessionStateKey<string> LAUNCHER_WORKSPACE_NAME_STATE_KEY = new(nameof(launcherWorkspaceName));
private static readonly AssistantSessionStateKey<string> LAUNCHER_PROVIDER_ID_STATE_KEY = new(nameof(launcherProviderId));
private static readonly AssistantSessionStateKey<string> LAUNCHER_PROFILE_ID_STATE_KEY = new(nameof(launcherProfileId));
private static readonly AssistantSessionStateKey<HashSet<string>?> LAUNCHER_PROFILE_IDS_STATE_KEY = new(nameof(launcherProfileIds));
private static readonly AssistantSessionStateKey<string> LAUNCHER_CHAT_TEMPLATE_ID_STATE_KEY = new(nameof(launcherChatTemplateId));
private static readonly AssistantSessionStateKey<List<string>> LAUNCHER_DATA_SOURCE_IDS_STATE_KEY = new(nameof(launcherDataSourceIds));
private static readonly AssistantSessionStateKey<HashSet<string>> LAUNCHER_TOOL_IDS_STATE_KEY = new(nameof(launcherToolIds));
@ -243,7 +243,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
this.descriptionSuggestion = string.Empty;
this.launcherWorkspaceName = string.Empty;
this.launcherProviderId = string.Empty;
this.launcherProfileId = string.Empty;
this.launcherProfileIds = null;
this.launcherChatTemplateId = string.Empty;
this.launcherDataSourceIds = [];
this.launcherToolIds = [];
@ -280,7 +280,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
state.Set(DESCRIPTION_SUGGESTION_STATE_KEY, this.descriptionSuggestion);
state.Set(LAUNCHER_WORKSPACE_NAME_STATE_KEY, this.launcherWorkspaceName);
state.Set(LAUNCHER_PROVIDER_ID_STATE_KEY, this.launcherProviderId);
state.Set(LAUNCHER_PROFILE_ID_STATE_KEY, this.launcherProfileId);
state.Set(LAUNCHER_PROFILE_IDS_STATE_KEY, this.launcherProfileIds);
state.Set(LAUNCHER_CHAT_TEMPLATE_ID_STATE_KEY, this.launcherChatTemplateId);
state.SetList(LAUNCHER_DATA_SOURCE_IDS_STATE_KEY, this.launcherDataSourceIds);
state.SetHashSet(LAUNCHER_TOOL_IDS_STATE_KEY, this.launcherToolIds);
@ -322,7 +322,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
state.Restore(DESCRIPTION_SUGGESTION_STATE_KEY, value => this.descriptionSuggestion = value);
state.Restore(LAUNCHER_WORKSPACE_NAME_STATE_KEY, value => this.launcherWorkspaceName = value);
state.Restore(LAUNCHER_PROVIDER_ID_STATE_KEY, value => this.launcherProviderId = value);
state.Restore(LAUNCHER_PROFILE_ID_STATE_KEY, value => this.launcherProfileId = value);
state.Restore(LAUNCHER_PROFILE_IDS_STATE_KEY, value => this.launcherProfileIds = value);
state.Restore(LAUNCHER_CHAT_TEMPLATE_ID_STATE_KEY, value => this.launcherChatTemplateId = value);
state.Restore(LAUNCHER_DATA_SOURCE_IDS_STATE_KEY, value => this.launcherDataSourceIds = value);
state.Restore(LAUNCHER_TOOL_IDS_STATE_KEY, value => this.launcherToolIds = ToolSelectionRules.NormalizeSelection(value));
@ -553,7 +553,7 @@ public partial class AssistantBuilder : AssistantBaseCore<NoSettingsPanel>
return new(
this.launcherWorkspaceName.Trim(),
NullIfEmpty(this.launcherProviderId),
NullIfEmpty(this.launcherProfileId),
this.launcherProfileIds?.Order(StringComparer.OrdinalIgnoreCase).ToArray(),
NullIfEmpty(this.launcherChatTemplateId),
dataSourceIds.Length == 0 ? null : dataSourceIds,
toolIds.Length == 0 ? null : toolIds);

View File

@ -4,8 +4,8 @@ internal sealed class AssistantBuilderChatLaunchMetadata
{
public string WorkspaceName { get; init; } = string.Empty;
public string? ProviderId { get; init; }
public string? ProfileId { get; init; }
public string[]? ProfileIds { get; init; }
public string? ChatTemplateId { get; init; }
public string[]? DataSourceIds { get; init; }
public string[]? ToolIds { get; init; }
}
}

View File

@ -128,7 +128,7 @@ internal sealed partial class LuaResponse
return false;
if (!IsOptionalGuid(launch.ProviderId, allowEmpty: false) ||
!IsOptionalGuid(launch.ProfileId, allowEmpty: true) ||
!IsOptionalGuidList(launch.ProfileIds, allowEmptyList: true) ||
!IsOptionalGuid(launch.ChatTemplateId, allowEmpty: true))
return false;
@ -154,6 +154,11 @@ internal sealed partial class LuaResponse
private static bool IsOptionalGuid(string? value, bool allowEmpty) => value is null ||
Guid.TryParse(value, out var parsed) && (allowEmpty || parsed != Guid.Empty);
private static bool IsOptionalGuidList(string[]? values, bool allowEmptyList) => values is null ||
(allowEmptyList || values.Length > 0) &&
values.All(value => Guid.TryParse(value, out var parsed) && parsed != Guid.Empty) &&
values.Distinct(StringComparer.OrdinalIgnoreCase).Count() == values.Length;
/// <summary>
/// Reads the first complete JSON object out of a model answer that may carry text around it.
/// </summary>

View File

@ -110,7 +110,7 @@ else
<ConfigurationProviderSelection Component="Components.DOCUMENT_ANALYSIS_ASSISTANT" Data="@this.availableLLMProviders" Disabled="@(() => this.IsNoPolicySelectedOrProtected)" SelectedValue="@(() => this.policyPreselectedProviderId)" SelectionUpdate="@this.PolicyPreselectedProviderWasChanged" ExplicitMinimumConfidence="@this.GetPolicyMinimumConfidenceLevel()"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => this.IsNoPolicySelected)" SelectedValue="@(() => this.policyPreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdateAsync="@(async selection => await this.PolicyPreselectedProfileWasChangedAsync(selection))" OptionHelp="@T("Choose whether the policy should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => this.IsNoPolicySelected)" SelectedProfileIds="@(() => this.policyPreselectedProfileIds)" SelectionUpdateAsync="@this.PolicyPreselectedProfilesWereChangedAsync" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
<MudTextSwitch Disabled="@(this.IsNoPolicySelected || (this.selectedPolicy?.IsEnterpriseConfiguration ?? true))" Label="@T("Would you like to protect this policy so that you cannot accidentally edit or delete it?")" Value="@this.policyIsProtected" ValueChanged="async state => await this.PolicyProtectionWasChanged(state)" LabelOn="@T("Yes, protect this policy")" LabelOff="@T("No, the policy can be edited")" />

View File

@ -143,7 +143,8 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
{
return new ChatThread
{
SystemPrompt = SystemPrompts.DEFAULT
SystemPrompt = SystemPrompts.DEFAULT,
SelectedProfileIds = [..this.CurrentProfileIds],
};
}
@ -152,6 +153,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
ChatId = Guid.NewGuid(),
Name = string.Format(T("{0} - Document Analysis Session"), this.selectedPolicy?.PolicyName ?? T("Empty")),
SystemPrompt = SystemPrompts.DEFAULT,
SelectedProfileIds = [..this.CurrentProfileIds],
Blocks =
[
// Replace the first "user block" (here, it was/is the block generated by the assistant) with a new one
@ -191,7 +193,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.policyMinimumProviderConfidence = ConfidenceLevel.NONE;
this.policyAllowedToolIds = [];
this.policyPreselectedProviderId = string.Empty;
this.policyPreselectedProfile = ProfilePreselection.NoProfile;
this.policyPreselectedProfileIds = [];
}
}
@ -219,7 +221,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.policyMinimumProviderConfidence = this.selectedPolicy.MinimumProviderConfidence;
this.policyAllowedToolIds = [..this.selectedPolicy.AllowedToolIds];
this.policyPreselectedProviderId = this.selectedPolicy.PreselectedProvider;
this.policyPreselectedProfile = ProfilePreselection.FromStoredValue(this.selectedPolicy.PreselectedProfile);
this.policyPreselectedProfileIds = this.selectedPolicy.PreselectedProfileIds is null ? null : [..this.selectedPolicy.PreselectedProfileIds];
return true;
}
@ -258,7 +260,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return;
// The preselected profile is always user-adjustable, even for protected policies and enterprise configurations:
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
this.selectedPolicy.PreselectedProfileIds = this.policyPreselectedProfileIds is null ? null : [..this.policyPreselectedProfileIds];
// Enterprise configurations cannot be modified at all:
if(this.selectedPolicy.IsEnterpriseConfiguration)
@ -292,7 +294,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
private ConfidenceLevel policyMinimumProviderConfidence = ConfidenceLevel.NONE;
private HashSet<string> policyAllowedToolIds = [];
private string policyPreselectedProviderId = string.Empty;
private ProfilePreselection policyPreselectedProfile = ProfilePreselection.NoProfile;
private HashSet<string>? policyPreselectedProfileIds = [];
private HashSet<FileAttachment> loadedDocumentPaths = [];
private readonly List<ConfigurationSelectData<string>> availableLLMProviders = new();
private static readonly AssistantSessionStateKey<DataDocumentAnalysisPolicy?> SELECTED_POLICY_STATE_KEY = new(nameof(selectedPolicy));
@ -305,7 +307,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
private static readonly AssistantSessionStateKey<string> POLICY_OUTPUT_RULES_STATE_KEY = new(nameof(policyOutputRules));
private static readonly AssistantSessionStateKey<ConfidenceLevel> POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY = new(nameof(policyMinimumProviderConfidence));
private static readonly AssistantSessionStateKey<string> POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY = new(nameof(policyPreselectedProviderId));
private static readonly AssistantSessionStateKey<ProfilePreselection> POLICY_PRESELECTED_PROFILE_STATE_KEY = new(nameof(policyPreselectedProfile));
private static readonly AssistantSessionStateKey<HashSet<string>?> POLICY_PRESELECTED_PROFILE_IDS_STATE_KEY = new(nameof(policyPreselectedProfileIds));
private static readonly AssistantSessionStateKey<HashSet<FileAttachment>> LOADED_DOCUMENT_PATHS_STATE_KEY = new(nameof(loadedDocumentPaths));
private static readonly AssistantSessionStateKey<List<ConfigurationSelectData<string>>> AVAILABLE_LLM_PROVIDERS_STATE_KEY = new(nameof(availableLLMProviders));
@ -322,7 +324,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
state.Set(POLICY_OUTPUT_RULES_STATE_KEY, this.policyOutputRules);
state.Set(POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY, this.policyMinimumProviderConfidence);
state.Set(POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY, this.policyPreselectedProviderId);
state.Set(POLICY_PRESELECTED_PROFILE_STATE_KEY, this.policyPreselectedProfile);
state.Set(POLICY_PRESELECTED_PROFILE_IDS_STATE_KEY, this.policyPreselectedProfileIds);
state.SetHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths);
state.SetList(AVAILABLE_LLM_PROVIDERS_STATE_KEY, this.availableLLMProviders);
}
@ -340,7 +342,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
state.Restore(POLICY_OUTPUT_RULES_STATE_KEY, value => this.policyOutputRules = value);
state.Restore(POLICY_MINIMUM_PROVIDER_CONFIDENCE_STATE_KEY, value => this.policyMinimumProviderConfidence = value);
state.Restore(POLICY_PRESELECTED_PROVIDER_ID_STATE_KEY, value => this.policyPreselectedProviderId = value);
state.Restore(POLICY_PRESELECTED_PROFILE_STATE_KEY, value => this.policyPreselectedProfile = value);
state.Restore(POLICY_PRESELECTED_PROFILE_IDS_STATE_KEY, value => this.policyPreselectedProfileIds = value);
state.RestoreHashSet(LOADED_DOCUMENT_PATHS_STATE_KEY, this.loadedDocumentPaths);
state.RestoreList(AVAILABLE_LLM_PROVIDERS_STATE_KEY, this.availableLLMProviders);
}
@ -487,7 +489,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
if (this.ProviderSettings != Settings.Provider.NONE &&
this.ProviderSettings.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
{
this.CurrentProfile = this.ResolveProfileSelection();
this.CurrentProfileIds = this.ResolveProfileSelection();
return;
}
}
@ -497,7 +499,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
if (policyProvider != Settings.Provider.NONE && policyProvider.UsedLLMProvider.GetConfidence(this.SettingsManager).Level >= minimumLevel)
{
this.ProviderSettings = policyProvider;
this.CurrentProfile = this.ResolveProfileSelection();
this.CurrentProfileIds = this.ResolveProfileSelection();
return;
}
@ -507,7 +509,7 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
fallbackProvider = Settings.Provider.NONE;
this.ProviderSettings = fallbackProvider;
this.CurrentProfile = this.ResolveProfileSelection();
this.CurrentProfileIds = this.ResolveProfileSelection();
}
private ConfidenceLevel GetPolicyMinimumConfidenceLevel()
@ -524,23 +526,19 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return minimumLevel;
}
private Profile ResolveProfileSelection()
private HashSet<string> ResolveProfileSelection()
{
if (this.selectedPolicy is null)
return this.SettingsManager.GetPreselectedProfile(this.Component);
return this.SettingsManager.GetPreselectedProfiles(this.Component).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
var policyProfilePreselection = ProfilePreselection.FromStoredValue(this.selectedPolicy.PreselectedProfile);
if (policyProfilePreselection.DoNotPreselectProfile)
return Profile.NO_PROFILE;
var policyProfilePreselection = ProfilePreselection.FromStoredValue(this.selectedPolicy.PreselectedProfileIds);
if (policyProfilePreselection.DoNotPreselectProfiles)
return [];
if (policyProfilePreselection.UseSpecificProfile)
{
var policyProfile = this.SettingsManager.ConfigurationData.Profiles.FirstOrDefault(x => x.Id == policyProfilePreselection.SpecificProfileId);
if (policyProfile is not null)
return policyProfile;
}
if (policyProfilePreselection.UseSpecificProfiles)
return this.SettingsManager.ResolveProfiles(policyProfilePreselection.SpecificProfileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
return this.SettingsManager.GetAppPreselectedProfile();
return this.SettingsManager.GetAppPreselectedProfiles().Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
@ -571,13 +569,13 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
this.ApplyPolicyPreselection();
}
private async Task PolicyPreselectedProfileWasChangedAsync(ProfilePreselection selection)
private async Task PolicyPreselectedProfilesWereChangedAsync(HashSet<string>? selection)
{
this.policyPreselectedProfile = selection;
this.policyPreselectedProfileIds = selection is null ? null : [..selection];
if (this.selectedPolicy is not null)
this.selectedPolicy.PreselectedProfile = this.policyPreselectedProfile;
this.selectedPolicy.PreselectedProfileIds = this.policyPreselectedProfileIds is null ? null : [..this.policyPreselectedProfileIds];
this.CurrentProfile = this.ResolveProfileSelection();
this.CurrentProfileIds = this.ResolveProfileSelection();
await this.AutoSave();
}
@ -891,7 +889,9 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
return string.Empty;
var preselectedProvider = string.IsNullOrWhiteSpace(this.selectedPolicy.PreselectedProvider) ? string.Empty : this.selectedPolicy.PreselectedProvider;
var preselectedProfile = string.IsNullOrWhiteSpace(this.selectedPolicy.PreselectedProfile) ? string.Empty : this.selectedPolicy.PreselectedProfile;
var preselectedProfiles = this.selectedPolicy.PreselectedProfileIds is null
? string.Empty
: $"[\"PreselectedProfileIds\"] = {{ {string.Join(", ", this.selectedPolicy.PreselectedProfileIds.Select(profileId => LuaTools.ToLuaStringLiteral(profileId)))} }},";
var id = string.IsNullOrWhiteSpace(this.selectedPolicy.Id) ? Guid.NewGuid().ToString() : this.selectedPolicy.Id;
var allowedToolIds = string.Join(", ", this.selectedPolicy.AllowedToolIds.OrderBy(x => x, StringComparer.Ordinal).Select(x => LuaTools.ToLuaStringLiteral(x)));
@ -915,10 +915,10 @@ public partial class DocumentAnalysisAssistant : AssistantBaseCore<NoSettingsPan
-- still meet the confidence requirements of the provider in use.
["AllowedToolIds"] = { {{allowedToolIds}} },
-- Optional: preselect a provider or profile by ID.
-- Optional: preselect a provider or profiles by ID.
-- The IDs must exist in CONFIG["LLM_PROVIDERS"] or CONFIG["PROFILES"].
["PreselectedProvider"] = "{{preselectedProvider}}",
["PreselectedProfile"] = "{{preselectedProfile}}",
{{preselectedProfiles}}
-- Optional: hide the policy definition section in the UI.
-- When set to true, users will only see the document selection interface

View File

@ -400,7 +400,7 @@ else
{
var selection = profileSelection;
<div class="@selection.Class" style="@GetOptionalStyle(selection.Style)">
<ProfileFormSelection Validation="@(profile => this.ValidateProfileSelection(selection, profile))" @bind-Profile="@this.CurrentProfile" />
<ProfileFormSelection Validation="@(profileIds => this.ValidateProfileSelection(selection, profileIds))" @bind-ProfileIds="@this.CurrentProfileIds" />
</div>
}
break;

View File

@ -422,15 +422,36 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
? this.assistantState.ToLuaTable(rootComponent.Children)
: new LuaTable();
var profile = new LuaTable
IReadOnlyList<Profile> selectedProfiles = this.AllowProfiles ? this.SettingsManager.ResolveProfiles(this.CurrentProfileIds) : [];
var profiles = new LuaTable();
for (var index = 0; index < selectedProfiles.Count; index++)
{
["Name"] = this.CurrentProfile.Name,
["NeedToKnow"] = this.CurrentProfile.NeedToKnow,
["Actions"] = this.CurrentProfile.Actions,
["Num"] = this.CurrentProfile.Num,
};
state["profile"] = profile;
var profile = selectedProfiles[index];
profiles[index + 1] = new LuaTable
{
["Id"] = profile.Id,
["Name"] = profile.Name,
["NeedToKnow"] = profile.NeedToKnow,
["Actions"] = profile.Actions,
["Num"] = profile.Num,
};
}
state["profiles"] = profiles;
if (profiles.ArrayLength == 1)
state["profile"] = profiles[1];
else if (profiles.ArrayLength == 0)
{
state["profile"] = new LuaTable
{
["Id"] = Profile.NO_PROFILE.Id,
["Name"] = Profile.NO_PROFILE.Name,
["NeedToKnow"] = Profile.NO_PROFILE.NeedToKnow,
["Actions"] = Profile.NO_PROFILE.Actions,
["Num"] = Profile.NO_PROFILE.Num,
};
}
return state;
}
@ -642,9 +663,9 @@ public partial class AssistantDynamic : AssistantBaseCore<NoSettingsPanel>
this.assistantState.MultiSelect[fieldName] = values;
});
private string? ValidateProfileSelection(AssistantProfileSelection profileSelection, Profile? profile)
private string? ValidateProfileSelection(AssistantProfileSelection profileSelection, HashSet<string> profileIds)
{
if (profile != null && profile != Profile.NO_PROFILE) return null;
if (this.SettingsManager.ResolveProfiles(profileIds).Count > 0) return null;
return !string.IsNullOrWhiteSpace(profileSelection.ValidationMessage) ? profileSelection.ValidationMessage : this.T("Please select one of your profiles.");
}

View File

@ -1081,6 +1081,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::CODING::COMMONCODINGLANGUAGEEXTENSIONS::T
-- {0} - Document Analysis Session
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T108097007"] = "{0} - Document Analysis Session"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T1126291596"] = "Preselect profiles"
-- Use the analysis and output rules to define how the AI evaluates your documents and formats the results.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T1155482668"] = "Use the analysis and output rules to define how the AI evaluates your documents and formats the results."
@ -1129,12 +1132,6 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA
-- Load output rules from document
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2168201568"] = "Load output rules from document"
-- Choose whether the policy should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2214900121"] = "Choose whether the policy should use the app default profile, no profile, or a specific profile."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2322771068"] = "Preselect a profile"
-- The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T238145218"] = "The analysis rules specify what the AI should pay particular attention to while reviewing the documents you provide, and which aspects it should highlight or save. For example, if you want to extract the potential of green hydrogen for agriculture from a variety of general publications, you can explicitly define this in the analysis rules."
@ -1159,6 +1156,9 @@ UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTA
-- Expand this section to view and edit the policy definition.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T277813037"] = "Expand this section to view and edit the policy definition."
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Policy name
UI_TEXT_CONTENT["AISTUDIO::ASSISTANTS::DOCUMENTANALYSIS::DOCUMENTANALYSISASSISTANT::T2879019438"] = "Policy name"
@ -3736,21 +3736,30 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DATASOURCESELECTION::T86053874"] = "Avail
-- Tools (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1019749907"] = "Tools (Optional)"
-- Use a custom profile selection
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T110831829"] = "Use a custom profile selection"
-- {0} profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1238255445"] = "{0} profiles"
-- 1 profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1242468481"] = "1 profile"
-- These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1286170698"] = "These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use."
-- Chat provider
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T1648955896"] = "Chat provider"
-- Use no profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2205839602"] = "Use no profile"
-- Use no profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2059344659"] = "Use no profiles"
-- Use chat defaults
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2283174672"] = "Use chat defaults"
-- Existing workspace (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2364306588"] = "Existing workspace (Optional)"
-- Chat profile
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2412069346"] = "Chat profile"
-- {0} data source(s) selected
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T2777836629"] = "{0} data source(s) selected"
@ -3766,12 +3775,21 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T295876489"] = "W
-- Data sources (Optional)
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3259309302"] = "Data sources (Optional)"
-- Profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3632612423"] = "Profiles"
-- Chat profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3834001219"] = "Chat profiles"
-- Use the normal chat data source defaults
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T3898572329"] = "Use the normal chat data source defaults"
-- Use no chat template
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T4258819635"] = "Use no chat template"
-- No profiles selected
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T541000009"] = "No profiles selected"
-- Chat template
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::DIRECTCHATLAUNCHERFORM::T923285303"] = "Chat template"
@ -3976,21 +3994,48 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T3451939995"] =
-- Release candidates are the final step before a feature is proven to be stable.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PREVIEWRELEASECANDIDATE::T696585888"] = "Release candidates are the final step before a feature is proven to be stable."
-- Select one of your profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T2003449133"] = "Select one of your profiles"
-- {0} profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T1238255445"] = "{0} profiles"
-- Select your profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T1981865790"] = "Select your profiles"
-- Open Profile Options
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T3654011106"] = "Open Profile Options"
-- No profiles selected
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEFORMSELECTION::T541000009"] = "No profiles selected"
-- Use a custom profile selection
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEPRESELECTIONCONFIGURATION::T110831829"] = "Use a custom profile selection"
-- Use no profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEPRESELECTIONCONFIGURATION::T2059344659"] = "Use no profiles"
-- Profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEPRESELECTIONCONFIGURATION::T3632612423"] = "Profiles"
-- Use app default
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEPRESELECTIONCONFIGURATION::T3672477670"] = "Use app default"
-- No profiles selected
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILEPRESELECTIONCONFIGURATION::T541000009"] = "No profiles selected"
-- {0} profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T1238255445"] = "{0} profiles"
-- Clear all
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T2038076485"] = "Clear all"
-- You can select your profiles here
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T2330383897"] = "You can select your profiles here"
-- Manage your profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3609533889"] = "Manage your profiles"
-- Open Profile Options
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T3654011106"] = "Open Profile Options"
-- You can switch between your profiles here
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROFILESELECTION::T918741365"] = "You can switch between your profiles here"
-- Audio input possible
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::PROVIDERSELECTION::T1742581112"] = "Audio input possible"
@ -4255,6 +4300,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1059411425"]
-- Do you want to show preview features in the app?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1118505044"] = "Do you want to show preview features in the app?"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1126291596"] = "Preselect profiles"
-- AI Studio cannot check for updates when running as a Flatpak. Updates are managed outside the app.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1190632518"] = "AI Studio cannot check for updates when running as a Flatpak. Updates are managed outside the app."
@ -4285,15 +4333,15 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T14786838"] =
-- A dialog lists what was removed and explains the attack pattern
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T148008546"] = "A dialog lists what was removed and explains the attack pattern"
-- You have selected {0} profiles.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1497583345"] = "You have selected {0} profiles."
-- Select the desired behavior for the navigation bar.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1555038969"] = "Select the desired behavior for the navigation bar."
-- Color theme
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1599198973"] = "Color theme"
-- Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1666052109"] = "Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence."
-- seconds
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T1723256298"] = "seconds"
@ -4342,6 +4390,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2341504363"]
-- Update installation method
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T237706157"] = "Update installation method"
-- Choose the profiles used by default throughout the app. A component-specific selection replaces this set completely.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2548951489"] = "Choose the profiles used by default throughout the app. A component-specific selection replaces this set completely."
-- Language
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2591284123"] = "Language"
@ -4363,6 +4414,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2918560776"]
-- Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T2960110864"] = "Enter one host pattern per line. Exact hosts such as data.intra.example.org and one-label wildcards such as *.intra.example.org are supported. Cloud provider endpoints built into AI Studio, such as OpenAI, Google, etc., never use these additional root certificates."
-- You have selected 1 profile.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3060815309"] = "You have selected 1 profile."
-- Save energy?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3100928009"] = "Save energy?"
@ -4408,9 +4462,6 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3914529369"]
-- Additional root certificates are disabled
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T3985928190"] = "Additional root certificates are disabled"
-- Preselect one of your profiles?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4004501229"] = "Preselect one of your profiles?"
-- When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T4067492921"] = "When enabled, spellchecking will be active in all input fields. Depending on your operating system, errors may not be visually highlighted, but right-clicking may still offer possible corrections."
@ -4444,6 +4495,9 @@ UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T602293588"]
-- Choose the color theme that best suits for you.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T654667432"] = "Choose the color theme that best suits for you."
-- No profiles selected.
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T707736869"] = "No profiles selected."
-- Should updates be installed automatically or manually?
UI_TEXT_CONTENT["AISTUDIO::COMPONENTS::SETTINGS::SETTINGSPANELAPP::T707880477"] = "Should updates be installed automatically or manually?"
@ -6856,6 +6910,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3909191077"] = "Yo
-- Remove this attachment.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::REVIEWATTACHMENTSDIALOG::T3933470258"] = "Remove this attachment."
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1126291596"] = "Preselect profiles"
-- There is no social event
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1222800281"] = "There is no social event"
@ -6877,9 +6934,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1471770981"
-- Preselect whether participants needs to arrive and depart
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1648427207"] = "Preselect whether participants needs to arrive and depart"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Preselect a start time?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1901151023"] = "Preselect a start time?"
@ -6892,9 +6946,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T1998244307"
-- Preselect whether the meeting is virtual
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2084951012"] = "Preselect whether the meeting is virtual"
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2322771068"] = "Preselect a profile"
-- When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2373110543"] = "When enabled, you can preselect most agenda options. This is might be useful when you need to create similar agendas often."
@ -6904,6 +6955,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2519703500"
-- Which agenda language should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2801220321"] = "Which agenda language should be preselected?"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Preselect another agenda language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T2915422331"] = "Preselect another agenda language"
@ -6976,24 +7030,21 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T816053055"]
-- Preselect whether the participants should actively involved
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGAGENDA::T817726429"] = "Preselect whether the participants should actively involved"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1126291596"] = "Preselect profiles"
-- Restrict to one bias a day?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1608129203"] = "Restrict to one bias a day?"
-- Yes, you can only retrieve one bias per day
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1765683725"] = "Yes, you can only retrieve one bias per day"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Reset
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T180921696"] = "Reset"
-- No restriction. You can retrieve as many biases as you want per day.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2305356277"] = "No restriction. You can retrieve as many biases as you want per day."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2322771068"] = "Preselect a profile"
-- Which language should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2345162613"] = "Which language should be preselected?"
@ -7006,6 +7057,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2382
-- Preselect the language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2571465005"] = "Preselect the language"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGASSISTANTBIAS::T3448155331"] = "Close"
@ -7186,6 +7240,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T74
-- Leave empty when an input folder should be selected for every batch run.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGBATCHPROCESSING::T762890100"] = "Leave empty when an input folder should be selected for every batch run."
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1126291596"] = "Preselect profiles"
-- Preselect one of your chat templates?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1402022556"] = "Preselect one of your chat templates?"
@ -7201,12 +7258,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1773585398"]
-- Provider selection when creating new chats
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T189306836"] = "Provider selection when creating new chats"
-- Choose whether chats should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T1915793195"] = "Choose whether chats should use the app default profile, no profile, or a specific profile."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2322771068"] = "Preselect a profile"
-- Apply default data source option when sending assistant results to chat
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2510376349"] = "Apply default data source option when sending assistant results to chat"
@ -7216,6 +7267,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T263621180"] =
-- Provider selection when loading a chat and sending assistant results to chat
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2868379953"] = "Provider selection when loading a chat and sending assistant results to chat"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Show the latest message after loading?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHAT::T2913693228"] = "Show the latest message after loading?"
@ -7321,15 +7375,15 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCHATTEMPLATE::T97542
-- Compiler messages are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1110902070"] = "Compiler messages are preselected"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2322771068"] = "Preselect a profile"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T1126291596"] = "Preselect profiles"
-- Preselect coding options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2790579667"] = "Preselect coding options?"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Preselect compiler messages?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGCODING::T2970689954"] = "Preselect compiler messages?"
@ -7456,23 +7510,23 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820
-- Local Directory
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1126291596"] = "Preselect profiles"
-- When enabled, you can preselect some ERI server options.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options."
-- Preselect ERI server options?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1664055662"] = "Preselect ERI server options?"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- No ERI server options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1793785587"] = "No ERI server options are preselected"
-- Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2093534613"] = "Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2322771068"] = "Preselect a profile"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T3448155331"] = "Close"
@ -7612,6 +7666,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1013787
-- Web content reader is shown
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1030372436"] = "Web content reader is shown"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1126291596"] = "Preselect profiles"
-- When enabled, the web content reader is preselected. This is might be useful when you prefer to load legal content from the web very often.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1507288278"] = "When enabled, the web content reader is preselected. This is might be useful when you prefer to load legal content from the web very often."
@ -7627,9 +7684,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1633101
-- Web content reader is not preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1701127912"] = "Web content reader is not preselected"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Content cleaner agent is not preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T1969816694"] = "Content cleaner agent is not preselected"
@ -7639,9 +7693,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2090693
-- When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2164667361"] = "When enabled, you can preselect some legal check options. This is might be useful when you prefer a specific LLM model."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2322771068"] = "Preselect a profile"
-- Legal check options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T252916114"] = "Legal check options are preselected"
@ -7651,6 +7702,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2746583
-- Web content reader is hidden
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2799795311"] = "Web content reader is hidden"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T3448155331"] = "Close"
@ -7666,11 +7720,8 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T4033382
-- Preselect the web content reader?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGLEGALCHECK::T629158142"] = "Preselect the web content reader?"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2322771068"] = "Preselect a profile"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T1126291596"] = "Preselect profiles"
-- Which language should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2345162613"] = "Which language should be preselected?"
@ -7681,6 +7732,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2382415529
-- Preselect the language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2571465005"] = "Preselect the language"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Close
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGMYTASKS::T3448155331"] = "Close"
@ -7822,6 +7876,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGREWRITE::T553954963"
-- Preselect the audience expertise
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1017131030"] = "Preselect the audience expertise"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1126291596"] = "Preselect profiles"
-- When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1393378753"] = "When enabled, you can preselect slide builder options. This is might be useful when you prefer a specific language or LLM model."
@ -7834,18 +7891,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T15493
-- No Slide Planner Assistant options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1694374279"] = "No Slide Planner Assistant options are preselected"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Preselect the audience organizational level
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2014662371"] = "Preselect the audience organizational level"
-- Which audience organizational level should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T216511105"] = "Which audience organizational level should be preselected?"
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2322771068"] = "Preselect a profile"
-- Which language should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2345162613"] = "Which language should be preselected?"
@ -7858,6 +7909,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T25714
-- Preselect the audience age group
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2645589441"] = "Preselect the audience age group"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- Assistant: Slide Planner Assistant Options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGSLIDEBUILDER::T3226042276"] = "Assistant: Slide Planner Assistant Options"
@ -8068,6 +8122,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGTRANSLATION::T918172
-- Source references are hidden
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T1087183156"] = "Source references are hidden"
-- Default profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T1577807404"] = "Default profiles"
-- Default target language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T1807183063"] = "Default target language"
@ -8104,9 +8161,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T428
-- Source references are visible
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T864087250"] = "Source references are visible"
-- Default profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T956261591"] = "Default profile"
-- Default audience profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGVISUALBRIEFING::T963676741"] = "Default audience profile"
@ -8134,6 +8188,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWORKSPACES::T4048028
-- Workspace maintenance
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWORKSPACES::T49653413"] = "Workspace maintenance"
-- Preselect profiles
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1126291596"] = "Preselect profiles"
-- Which writing style should be preselected?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1173034744"] = "Which writing style should be preselected?"
@ -8146,9 +8203,6 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1417
-- Preselect another target language
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1462295644"] = "Preselect another target language"
-- Choose whether the assistant should use the app default profile, no profile, or a specific profile.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T1766361623"] = "Choose whether the assistant should use the app default profile, no profile, or a specific profile."
-- Assistant: Writing E-Mails Options
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2021226503"] = "Assistant: Writing E-Mails Options"
@ -8158,12 +8212,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2116
-- Preselect your name for the closing salutation?
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T221974240"] = "Preselect your name for the closing salutation?"
-- Preselect a profile
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2322771068"] = "Preselect a profile"
-- Preselect a writing style
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T28456020"] = "Preselect a writing style"
-- Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2872550582"] = "Use the app default profiles, no profiles, or a custom selection that completely replaces the app default."
-- E-Mail options are preselected
UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGWRITINGEMAILS::T2985974420"] = "E-Mail options are preselected"
@ -9805,9 +9859,6 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3491430707
-- Install updates automatically
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3569059463"] = "Install updates automatically"
-- Use app default profile
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3587225583"] = "Use app default profile"
-- Disable workspaces
UI_TEXT_CONTENT["AISTUDIO::SETTINGS::CONFIGURATIONSELECTDATAFACTORY::T3612390107"] = "Disable workspaces"
@ -10528,9 +10579,15 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1
-- The provided ASSISTANT lua table does not contain a valid UI table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T1841068402"] = "The provided ASSISTANT lua table does not contain a valid UI table."
-- The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2303567771"] = "The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs."
-- The provided ASSISTANT lua table does not contain a valid description.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2514141654"] = "The provided ASSISTANT lua table does not contain a valid description."
-- The ASSISTANT table contains both ProfileId and ProfileIds. Use only one of them.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2567419456"] = "The ASSISTANT table contains both ProfileId and ProfileIds. Use only one of them."
-- The provided ASSISTANT lua table does not contain a valid title.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::ASSISTANTS::PLUGINASSISTANTS::T2814605990"] = "The provided ASSISTANT lua table does not contain a valid title."
@ -10822,9 +10879,21 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCATEGORYEXTENSIONS::T91464
-- The SETTINGS table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1148682011"] = "The SETTINGS table does not exist or is not a valid table."
-- The configured visual briefing profile preselection is invalid.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T1806426557"] = "The configured visual briefing profile preselection is invalid."
-- The configured app profile preselection is invalid.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T2768414424"] = "The configured app profile preselection is invalid."
-- The CONFIG table does not exist or is not a valid table.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T3331620576"] = "The CONFIG table does not exist or is not a valid table."
-- The SETTINGS table contains both '{0}' and '{1}'. Use only one of them.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T4104310827"] = "The SETTINGS table contains both '{0}' and '{1}'. Use only one of them."
-- The configured chat profile preselection is invalid.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINCONFIGURATION::T4292017635"] = "The configured chat profile preselection is invalid."
-- The field IETF_TAG does not exist or is not a valid string.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::PLUGINSYSTEM::PLUGINLANGUAGE::T1796010240"] = "The field IETF_TAG does not exist or is not a valid string."
@ -11170,8 +11239,8 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::ASSISTANTPLUGINGENERATIONSERVICE::T6
-- The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T103791004"] = "The following data sources selected by the assistant chat launcher are currently unavailable or not permitted for the selected provider: {0}"
-- The assistant chat launcher references profile '{0}', but that profile does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2466659933"] = "The assistant chat launcher references profile '{0}', but that profile does not exist."
-- The assistant chat launcher references one or more profiles that do not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T2271819418"] = "The assistant chat launcher references one or more profiles that do not exist."
-- The assistant chat launcher references data source '{0}', but that data source does not exist.
UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::DIRECTCHATSERVICE::T289191545"] = "The assistant chat launcher references data source '{0}', but that data source does not exist."

View File

@ -1,7 +1,7 @@
@attribute [Route(Routes.ASSISTANT_MY_TASKS)]
@inherits AssistantBaseCore<AIStudio.Dialogs.Settings.SettingsDialogMyTasks>
<ProfileFormSelection Validation="@this.ValidateProfile" @bind-Profile="@this.CurrentProfile"/>
<ProfileFormSelection Validation="@this.ValidateProfiles" @bind-ProfileIds="@this.CurrentProfileIds"/>
<MudTextField T="string" @bind-Text="@this.inputText" Validation="@this.ValidatingText" AdornmentIcon="@Icons.Material.Filled.DocumentScanner" Adornment="Adornment.Start" Label="@T("Text or email")" Variant="Variant.Outlined" Lines="12" AutoGrow="@true" MaxLines="24" Class="mb-3" UserAttributes="@USER_INPUT_ATTRIBUTES"/>
<MudText Typo="Typo.h6" Class="mb-1 mt-1">@T("Attach documents")</MudText>
<MudJustifiedText Typo="Typo.body1" Class="mb-2">

View File

@ -164,9 +164,9 @@ public partial class AssistantMyTasks : AssistantBaseCore<SettingsDialogMyTasks>
await this.Form.Validate();
}
private string? ValidateProfile(Profile profile)
private string? ValidateProfiles(HashSet<string> profileIds)
{
if(profile == Profile.NO_PROFILE)
if(this.SettingsManager.ResolveProfiles(profileIds).Count == 0)
return T("Please select one of your profiles.");
return null;

View File

@ -88,7 +88,8 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
{
return new ChatThread
{
SystemPrompt = SystemPrompts.DEFAULT
SystemPrompt = SystemPrompts.DEFAULT,
SelectedProfileIds = [..this.CurrentProfileIds],
};
}
@ -97,6 +98,7 @@ public partial class SlideAssistant : AssistantBaseCore<SettingsDialogSlideBuild
ChatId = Guid.NewGuid(),
Name = string.Format(T("{0} - Slide Builder Session"), this.inputTitle),
SystemPrompt = SystemPrompts.DEFAULT,
SelectedProfileIds = [..this.CurrentProfileIds],
Blocks =
[
// Visible user block:

View File

@ -18,7 +18,7 @@ internal sealed class StructuredLlmStageRunner(
/// </summary>
/// <typeparam name="T">The strict response type.</typeparam>
/// <param name="provider">The selected provider configuration.</param>
/// <param name="profile">The selected user profile.</param>
/// <param name="profiles">The selected user profiles.</param>
/// <param name="systemContract">The stage-specific system contract.</param>
/// <param name="prompt">The user prompt containing stage inputs.</param>
/// <param name="attachments">The first-turn attachments.</param>
@ -30,7 +30,7 @@ internal sealed class StructuredLlmStageRunner(
/// <returns>The validated stage result.</returns>
public async Task<StructuredLlmStageResult<T>> RunAsync<T>(
ProviderSettings provider,
Profile profile,
IReadOnlyList<Profile> profiles,
string systemContract,
string prompt,
IReadOnlyList<FileAttachment> attachments,
@ -54,8 +54,7 @@ internal sealed class StructuredLlmStageRunner(
Before sending, silently verify that the root object is closed and every property conforms to the grammar.
Answer with the bare JSON object and nothing else: no explanation, no Markdown, and no code fence.
User profile:
{profile.ToSystemPrompt()}
{Profile.ToSystemPrompt(profiles)}
""";
var time = DateTimeOffset.UtcNow;
@ -286,4 +285,4 @@ internal sealed class StructuredLlmStageRunner(
/// <param name="eventId">The stable event identifier.</param>
/// <returns>The logging event.</returns>
private static EventId Event(VisualBriefingLogEventId eventId) => new((int)eventId, eventId.ToString());
}
}

View File

@ -294,9 +294,9 @@ public sealed partial class VisualBriefingArtifactService
var sensitiveValues = new[]
{
manifest.Settings.ProviderId,
manifest.Settings.ProfileId,
manifest.Settings.ModelId,
}
.Concat(manifest.Settings.ProfileIds)
.Where(candidate => !string.IsNullOrWhiteSpace(candidate));
return sensitiveValues.Any(candidate => text.Contains(candidate, StringComparison.Ordinal));
}
@ -531,4 +531,4 @@ public sealed partial class VisualBriefingArtifactService
[GeneratedRegex("""\[\s*(?<name>[A-Za-z_:][A-Za-z0-9_:.-]*)\s*(?:(?<operator>[~|^$*]?=)\s*(?:"(?<double>[^"]*)"|'(?<single>[^']*)'|(?<unquoted>[^\]\s]+))\s*(?<modifier>[iIsS])?\s*)?\]""", RegexOptions.CultureInvariant)]
private static partial Regex AttributeSelectorRegex();
}
}

View File

@ -211,7 +211,7 @@
}
</MudStack>
<ProfileFormSelection @bind-Profile="@this.editor.Profile" Disabled="@this.IsCurrentBusy"/>
<ProfileFormSelection @bind-ProfileIds="@this.editor.ProfileIds" Disabled="@this.IsCurrentBusy"/>
<EnumSelection T="CommonLanguages" NameFunc="@(language => language.Name())" @bind-Value="@this.editor.TargetLanguage" Icon="@Icons.Material.Filled.Translate" Label="@T("Target language")" AllowOther="true" @bind-OtherInput="@this.editor.CustomTargetLanguage" OtherValue="CommonLanguages.OTHER" LabelOther="@T("Custom target language")" ValidateOther="@this.ValidateCustomTargetLanguage" SelectionUpdated="@(_ => this.ScheduleFormValidation())" Disabled="@this.IsCurrentBusy"/>
<EnumSelection T="AudienceProfile" NameFunc="@(value => value.Name())" @bind-Value="@this.editor.AudienceProfile" Label="@T("Audience profile")" Disabled="@this.IsCurrentBusy"/>
<EnumSelection T="AudienceAgeGroup" NameFunc="@(value => value.Name())" @bind-Value="@this.editor.AudienceAgeGroup" Label="@T("Audience age group")" Disabled="@this.IsCurrentBusy"/>
@ -337,4 +337,4 @@
</main>
</div>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@ -163,10 +163,10 @@ public partial class VisualBriefingAssistant
var generationBriefing = this.selectedBriefing;
var parentRevisionId = parentRevisionOverride ?? (generationBriefing.Versions.Count == 0 ? null : this.selectedRevisionId);
var generationProvider = this.editor.Provider;
var generationProfile = this.editor.Profile;
var generationProfiles = this.SettingsManager.ResolveProfiles(this.editor.ProfileIds);
await this.RunBriefingOperationAsync(generationBriefing, mode, token => this.BuildOrchestrator.BuildAsync(generationBriefing, mode,
parentRevisionId, generationProvider, generationProfile, reusableBuildId, token),
parentRevisionId, generationProvider, generationProfiles, reusableBuildId, token),
T("A new visual briefing version was created."),
T("The visual briefing generation was canceled."),
T("The visual briefing operation failed unexpectedly. Copy the technical details for support."));
@ -314,4 +314,4 @@ public partial class VisualBriefingAssistant
/// Creates the assistant-session key used by a visual briefing build.
/// </summary>
private static AssistantSessionKey CreateBuildSessionKey(Guid briefingId) => new(ComponentKind.VISUAL_BRIEFING_ASSISTANT, briefingId.ToString("D"));
}
}

View File

@ -62,13 +62,13 @@ public partial class VisualBriefingAssistant
{
var defaults = this.SettingsManager.ConfigurationData.VisualBriefing;
var defaultProvider = this.SettingsManager.GetPreselectedProvider(ComponentKind.VISUAL_BRIEFING_ASSISTANT);
var defaultProfile = this.SettingsManager.GetPreselectedProfile(ComponentKind.VISUAL_BRIEFING_ASSISTANT);
var defaultProfiles = this.SettingsManager.GetPreselectedProfiles(ComponentKind.VISUAL_BRIEFING_ASSISTANT);
var suggestedName = string.Format(T("Briefing {0}"), DateTimeOffset.Now.ToString("yyyy-MM-dd HH:mm"));
var settings = new VisualBriefingLocalSettings
{
ProviderId = defaultProvider.Id,
ModelId = defaultProvider.Model.Id,
ProfileId = defaultProfile.Id,
ProfileIds = defaultProfiles.Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase),
TargetLanguage = defaults.PreselectedTargetLanguage,
CustomTargetLanguage = defaults.PreselectedOtherLanguage,
AudienceProfile = defaults.PreselectedAudienceProfile,
@ -386,4 +386,4 @@ public partial class VisualBriefingAssistant
Settings = this.editor.ToSettings(),
Sources = this.editor.ToSources().Select(source => new { source.Path, source.Kind }).ToArray(),
}, VisualBriefingJson.Canonical);
}
}

View File

@ -189,7 +189,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
VisualBriefingEditMode mode,
Guid? parentRevisionId,
ProviderSettings provider,
Profile profile,
IReadOnlyList<Profile> profiles,
string sourceFingerprint,
string? reusedContentHash) =>
VisualBriefingHashing.ComputeSections(
@ -197,7 +197,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
parentRevisionId?.ToString("D"),
provider.Id,
provider.Model.Id,
profile.Id,
string.Join(";", profiles.Select(profile => profile.Id)),
sourceFingerprint,
VisualBriefingHashing.Compute(manifest.Settings.Instruction),
manifest.Settings.TargetLanguage.ToString(),

View File

@ -85,11 +85,11 @@ internal sealed partial class VisualBriefingBuildOrchestrator
/// <param name="mode">The edit mode.</param>
/// <param name="parentRevisionId">The selected parent revision.</param>
/// <param name="provider">The selected provider.</param>
/// <param name="profile">The selected profile.</param>
/// <param name="profiles">The selected profiles.</param>
/// <param name="reusableContentBuildId">An incompatible update build whose content should be reused as a rebuild.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The terminal build result.</returns>
public async Task<VisualBriefingBuildResult> BuildAsync(VisualBriefingManifest manifest, VisualBriefingEditMode mode, Guid? parentRevisionId, ProviderSettings provider, Profile profile, Guid? reusableContentBuildId = null, CancellationToken token = default)
public async Task<VisualBriefingBuildResult> BuildAsync(VisualBriefingManifest manifest, VisualBriefingEditMode mode, Guid? parentRevisionId, ProviderSettings provider, IReadOnlyList<Profile> profiles, Guid? reusableContentBuildId = null, CancellationToken token = default)
{
var operationId = Guid.NewGuid();
var proposedBuildId = Guid.NewGuid();
@ -142,7 +142,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
VisualBriefingEvidenceStage.ComputeInputFingerprint(
manifest,
provider,
profile,
profiles,
sourceFingerprint),
reusableEvidenceInputFingerprint,
StringComparison.Ordinal)))
@ -152,7 +152,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
"The sources or evidence settings changed after the evidence was validated. Start a full rebuild.",
$"EvidenceArtifactId={reusableEvidence.ArtifactId:D}; Rule={VisualBriefingValidationRule.REFERENCE_INVALID}.");
var inputFingerprint = ComputeBuildInputFingerprint(manifest, mode, parentRevisionId, provider, profile, sourceFingerprint, reusableEvidence?.PayloadHash);
var inputFingerprint = ComputeBuildInputFingerprint(manifest, mode, parentRevisionId, provider, profiles, sourceFingerprint, reusableEvidence?.PayloadHash);
var now = DateTimeOffset.UtcNow;
var candidate = new VisualBriefingBuildRecord
{
@ -247,7 +247,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
else
{
diagnostics.Stage = VisualBriefingBuildStage.EVIDENCE;
evidence = await this.evidenceStage.ExecuteAsync(manifest, provider, profile, prepared!, build, token);
evidence = await this.evidenceStage.ExecuteAsync(manifest, provider, profiles, prepared!, build, token);
}
diagnostics.ContentHashes["evidence"] = evidence.PayloadHash;
@ -265,7 +265,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
else
{
diagnostics.Stage = VisualBriefingBuildStage.PLAN;
plan = await this.planStage.ExecuteAsync(manifest, provider, profile, evidence, build, token);
plan = await this.planStage.ExecuteAsync(manifest, provider, profiles, evidence, build, token);
}
diagnostics.ContentHashes["plan"] = plan.PayloadHash;
@ -286,7 +286,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
try
{
content = await this.contentStage.ExecuteAsync(manifest, provider, profile, evidence, plan, build, token);
content = await this.contentStage.ExecuteAsync(manifest, provider, profiles, evidence, plan, build, token);
}
catch (VisualBriefingBuildException exception) when (mode is VisualBriefingEditMode.UPDATE_CONTENT && exception.Code is VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID && build.Failure?.ValidationRule is VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID)
{
@ -330,7 +330,7 @@ internal sealed partial class VisualBriefingBuildOrchestrator
else
{
diagnostics.Stage = VisualBriefingBuildStage.DESIGN;
presentation = await this.presentationStage.ExecuteAsync(manifest, provider, profile, plan, content, mode is VisualBriefingEditMode.CHANGE_DESIGN ? parentContext.Presentation : null, build, token);
presentation = await this.presentationStage.ExecuteAsync(manifest, provider, profiles, plan, content, mode is VisualBriefingEditMode.CHANGE_DESIGN ? parentContext.Presentation : null, build, token);
}
diagnostics.ContentHashes["design"] = presentation.PayloadHash;
@ -502,4 +502,4 @@ internal sealed partial class VisualBriefingBuildOrchestrator
/// <returns>A value task representing cleanup.</returns>
public async ValueTask DisposeAsync() => await dispose();
}
}
}

View File

@ -16,7 +16,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
/// </summary>
private const string SHOW_ALL_VALUE = "*";
public async Task<VisualBriefingContentArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, Profile profile, VisualBriefingEvidenceArtifact evidence, VisualBriefingPlanArtifact plan, VisualBriefingBuildRecord build, CancellationToken token)
public async Task<VisualBriefingContentArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, IReadOnlyList<Profile> profiles, VisualBriefingEvidenceArtifact evidence, VisualBriefingPlanArtifact plan, VisualBriefingBuildRecord build, CancellationToken token)
{
if (build.ContentArtifactId is { } completedId)
{
@ -29,7 +29,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
manifest.Settings.TargetLanguage.ToString(), manifest.Settings.CustomTargetLanguage, manifest.Settings.AudienceProfile.ToString(),
manifest.Settings.AudienceAgeGroup.ToString(), manifest.Settings.AudienceOrganizationalLevel.ToString(), manifest.Settings.AudienceExpertise.ToString(),
manifest.Settings.ShowSourceReferences.ToString(), SourceReferenceFingerprint(manifest), manifest.Settings.ProtectionLevel.ToString(),
manifest.Settings.CustomProtectionLevel, provider.Id, provider.Model.Id, profile.Id, VisualBriefingHashing.Compute(profile.ToSystemPrompt()),
manifest.Settings.CustomProtectionLevel, provider.Id, provider.Model.Id, string.Join(";", profiles.Select(profile => profile.Id)), VisualBriefingHashing.Compute(Profile.ToSystemPrompt(profiles)),
VisualBriefingVersions.CONTENT_CONTRACT.ToString());
var stage = VisualBriefingEvidenceStage.Start(build, VisualBriefingBuildStage.CONTENT, computedHash);
@ -37,7 +37,7 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingContentResponse>(provider, profile, BuildSystemContract(),
var run = await stageRunner.RunAsync<VisualBriefingContentResponse>(provider, profiles, BuildSystemContract(),
BuildPrompt(manifest, evidence, plan), [], VisualBriefingBuildStage.CONTENT, build.OperationId, build.BuildId,
response => this.ValidateResponseAndProject(manifest, plan, evidence, response), token);
@ -399,4 +399,4 @@ internal sealed class VisualBriefingContentStage(StructuredLlmStageRunner stageR
/// <see cref="RESET_LABEL"/>.
/// </summary>
private const string SHOW_ALL_LABEL = "Show all";
}
}

View File

@ -13,8 +13,7 @@ namespace AIStudio.Assistants.VisualBriefing;
/// <remarks>
/// This is the single source of truth for the briefing editor. It exists because the editor cannot
/// bind to <see cref="VisualBriefingLocalSettings"/> directly: that type stores the provider, model,
/// and profile as identifiers, while the UI binds whole <see cref="ProviderSettings"/> and
/// <see cref="Profile"/> objects. Keeping one draft object means saving, restoring, and change
/// and profiles as identifiers, while the UI binds the resolved provider and profile IDs. Keeping one draft object means saving, restoring, and change
/// detection all read the same fields instead of three hand-maintained lists.
/// </remarks>
public sealed class VisualBriefingEditorState
@ -28,8 +27,8 @@ public sealed class VisualBriefingEditorState
/// <summary>Gets or sets the selected provider and model.</summary>
public ProviderSettings Provider { get; set; } = ProviderSettings.NONE;
/// <summary>Gets or sets the selected profile.</summary>
public Profile Profile { get; set; } = Profile.NO_PROFILE;
/// <summary>Gets or sets the selected profile IDs.</summary>
public HashSet<string> ProfileIds { get; set; } = [];
/// <summary>Gets or sets the current scope or change instruction.</summary>
public string Instruction { get; set; } = string.Empty;
@ -93,7 +92,7 @@ public sealed class VisualBriefingEditorState
CustomProtectionLevel = briefing.Settings.CustomProtectionLevel,
Provider = ResolveProvider(briefing, settingsManager),
Profile = settingsManager.GetProfileById(briefing.Settings.ProfileId),
ProfileIds = settingsManager.ResolveProfiles(briefing.Settings.ProfileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase),
SourceMaterial =
[
@ -155,7 +154,7 @@ public sealed class VisualBriefingEditorState
{
ProviderId = this.Provider.Id,
ModelId = this.Provider.Model.Id,
ProfileId = this.Profile.Id,
ProfileIds = [..this.ProfileIds],
TargetLanguage = this.TargetLanguage,
CustomTargetLanguage = this.CustomTargetLanguage,
AudienceProfile = this.AudienceProfile,
@ -193,4 +192,4 @@ public sealed class VisualBriefingEditorState
.Select(attachment => attachment.FilePath)
.Order(StringComparer.Ordinal)
.Select(path => (path, kind));
}
}

View File

@ -19,12 +19,12 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
/// </summary>
/// <param name="manifest">The briefing manifest.</param>
/// <param name="provider">The selected provider and model.</param>
/// <param name="profile">The selected prompt profile.</param>
/// <param name="profiles">The selected prompt profiles.</param>
/// <param name="preparedSources">The validated prepared sources.</param>
/// <param name="build">The persistent build record.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The validated immutable evidence artifact.</returns>
public async Task<VisualBriefingEvidenceArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, Profile profile, VisualBriefingPreparedSources preparedSources, VisualBriefingBuildRecord build, CancellationToken token)
public async Task<VisualBriefingEvidenceArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, IReadOnlyList<Profile> profiles, VisualBriefingPreparedSources preparedSources, VisualBriefingBuildRecord build, CancellationToken token)
{
if (build.EvidenceArtifactId is { } completedId)
{
@ -33,12 +33,12 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
return completed;
}
var stage = Start(build, VisualBriefingBuildStage.EVIDENCE, ComputeInputFingerprint(manifest, provider, profile, preparedSources.SourceFingerprint));
var stage = Start(build, VisualBriefingBuildStage.EVIDENCE, ComputeInputFingerprint(manifest, provider, profiles, preparedSources.SourceFingerprint));
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingEvidenceResponse>(
provider, profile, BuildSystemContract(), BuildPrompt(manifest, preparedSources), preparedSources.Attachments, VisualBriefingBuildStage.EVIDENCE,
provider, profiles, BuildSystemContract(), BuildPrompt(manifest, preparedSources), preparedSources.Attachments, VisualBriefingBuildStage.EVIDENCE,
build.OperationId, build.BuildId, response => VisualBriefingValidation.ValidateEvidence(manifest, response), token);
stage.Attempts = run.Attempts;
@ -71,10 +71,10 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
return artifact;
}
internal static string ComputeInputFingerprint(VisualBriefingManifest manifest, ProviderSettings provider, Profile profile, string sourceFingerprint) =>
internal static string ComputeInputFingerprint(VisualBriefingManifest manifest, ProviderSettings provider, IReadOnlyList<Profile> profiles, string sourceFingerprint) =>
VisualBriefingHashing.ComputeSections(sourceFingerprint, VisualBriefingHashing.Compute(manifest.Settings.Instruction),
manifest.Settings.TargetLanguage.ToString(), manifest.Settings.CustomTargetLanguage, provider.Id,
provider.Model.Id, profile.Id, VisualBriefingHashing.Compute(profile.ToSystemPrompt()),
provider.Model.Id, string.Join(";", profiles.Select(profile => profile.Id)), VisualBriefingHashing.Compute(Profile.ToSystemPrompt(profiles)),
VisualBriefingVersions.EVIDENCE_CONTRACT.ToString());
private static string BuildSystemContract() =>
@ -192,4 +192,4 @@ internal sealed class VisualBriefingEvidenceStage(StructuredLlmStageRunner stage
? $"{details}."
: $"{details}; {diagnostic.ToTechnicalDetails()}.";
}
}
}

View File

@ -1,4 +1,7 @@
using AIStudio.Assistants.SlideBuilder;
using AIStudio.Settings;
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.VisualBriefing;
@ -18,9 +21,23 @@ public sealed class VisualBriefingLocalSettings
public string ModelId { get; set; } = string.Empty;
/// <summary>
/// Defines <c>ProfileId</c> for the visual briefing feature.
/// Defines <c>ProfileIds</c> for the visual briefing feature.
/// </summary>
public string ProfileId { get; set; } = string.Empty;
public HashSet<string> ProfileIds { get; set; } = [];
[JsonPropertyName("ProfileId")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LegacyProfileId
{
get => null;
set
{
if (this.ProfileIds.Count == 0 &&
!string.IsNullOrWhiteSpace(value) &&
!value.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
this.ProfileIds.Add(value);
}
}
/// <summary>
/// Defines <c>TargetLanguage</c> for the visual briefing feature.
@ -76,4 +93,4 @@ public sealed class VisualBriefingLocalSettings
/// Defines <c>CustomProtectionLevel</c> for the visual briefing feature.
/// </summary>
public string CustomProtectionLevel { get; set; } = string.Empty;
}
}

View File

@ -19,12 +19,12 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
/// </summary>
/// <param name="manifest">The briefing manifest.</param>
/// <param name="provider">The selected provider and model.</param>
/// <param name="profile">The selected prompt profile.</param>
/// <param name="profiles">The selected prompt profiles.</param>
/// <param name="evidence">The validated evidence artifact.</param>
/// <param name="build">The persistent build record.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>The validated immutable plan artifact.</returns>
public async Task<VisualBriefingPlanArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, Profile profile, VisualBriefingEvidenceArtifact evidence, VisualBriefingBuildRecord build, CancellationToken token)
public async Task<VisualBriefingPlanArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, IReadOnlyList<Profile> profiles, VisualBriefingEvidenceArtifact evidence, VisualBriefingBuildRecord build, CancellationToken token)
{
if (build.PlanArtifactId is { } completedId)
{
@ -36,13 +36,13 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
var stage = VisualBriefingEvidenceStage.Start(build, VisualBriefingBuildStage.PLAN, VisualBriefingHashing.ComputeSections(evidence.PayloadHash,
VisualBriefingHashing.Compute(manifest.Settings.Instruction), manifest.Settings.AudienceProfile.ToString(),
manifest.Settings.AudienceAgeGroup.ToString(), manifest.Settings.AudienceOrganizationalLevel.ToString(),
manifest.Settings.AudienceExpertise.ToString(), provider.Id, provider.Model.Id, profile.Id,
VisualBriefingHashing.Compute(profile.ToSystemPrompt()), VisualBriefingVersions.PLAN_CONTRACT.ToString()));
manifest.Settings.AudienceExpertise.ToString(), provider.Id, provider.Model.Id, string.Join(";", profiles.Select(profile => profile.Id)),
VisualBriefingHashing.Compute(Profile.ToSystemPrompt(profiles)), VisualBriefingVersions.PLAN_CONTRACT.ToString()));
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingPlanResponse>(provider, profile, BuildSystemContract(), BuildPrompt(manifest, evidence),
var run = await stageRunner.RunAsync<VisualBriefingPlanResponse>(provider, profiles, BuildSystemContract(), BuildPrompt(manifest, evidence),
[], VisualBriefingBuildStage.PLAN, build.OperationId, build.BuildId, response => VisualBriefingValidation.ValidatePlan(evidence, response), token);
stage.Attempts = run.Attempts;
@ -113,4 +113,4 @@ internal sealed class VisualBriefingPlanStage(StructuredLlmStageRunner stageRunn
Scope instruction: {manifest.Settings.Instruction}
Evidence: {JsonSerializer.Serialize(new { evidence.Facts, evidence.Metrics, evidence.Tables, evidence.AssetPlan }, VisualBriefingJson.Canonical)}
""";
}
}

View File

@ -11,7 +11,7 @@ namespace AIStudio.Assistants.VisualBriefing;
/// </summary>
internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner stageRunner, VisualBriefingStore store, VisualBriefingBuildProgressService progressService, ILogger<VisualBriefingPresentationStage> logger)
{
public async Task<VisualBriefingPresentationArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, Profile profile,
public async Task<VisualBriefingPresentationArtifact> ExecuteAsync(VisualBriefingManifest manifest, ProviderSettings provider, IReadOnlyList<Profile> profiles,
VisualBriefingPlanArtifact plan, VisualBriefingContentArtifact content, VisualBriefingPresentationArtifact? parentPresentation,
VisualBriefingBuildRecord build, CancellationToken token)
{
@ -34,15 +34,15 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s
parentPresentation?.PayloadHash ?? string.Empty,
provider.Id,
provider.Model.Id,
profile.Id,
VisualBriefingHashing.Compute(profile.ToSystemPrompt()),
string.Join(";", profiles.Select(profile => profile.Id)),
VisualBriefingHashing.Compute(Profile.ToSystemPrompt(profiles)),
VisualBriefingVersions.DESIGN_CONTRACT.ToString());
build.UpdatedAtUtc = DateTimeOffset.UtcNow;
await store.SaveBuildAsync(build, token);
progressService.Publish(build);
var run = await stageRunner.RunAsync<VisualBriefingDesignResponse>(provider, profile, BuildSystemContract(),
var run = await stageRunner.RunAsync<VisualBriefingDesignResponse>(provider, profiles, BuildSystemContract(),
BuildPrompt(manifest, plan, parentPresentation), [], VisualBriefingBuildStage.DESIGN, build.OperationId, build.BuildId,
response => ValidateDesign(manifest, plan, content, response), token);
@ -205,4 +205,4 @@ internal sealed class VisualBriefingPresentationStage(StructuredLlmStageRunner s
return record;
}
}
}

View File

@ -44,9 +44,27 @@ public sealed record ChatThread
public string SelectedProvider { get; set; } = string.Empty;
/// <summary>
/// Specifies the profile selected for the chat thread.
/// Specifies the profiles selected for the chat thread.
/// </summary>
public string SelectedProfile { get; set; } = string.Empty;
public HashSet<string> SelectedProfileIds { get; set; } = [];
/// <summary>
/// Permanently supports reading the singular profile field written by older app versions.
/// New chats write only <see cref="SelectedProfileIds"/>.
/// </summary>
[JsonPropertyName("SelectedProfile")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? LegacySelectedProfile
{
get => null;
set
{
if (this.SelectedProfileIds.Count == 0 &&
!string.IsNullOrWhiteSpace(value) &&
!value.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
this.SelectedProfileIds.Add(value);
}
}
/// <summary>
/// Specifies the profile selected for the chat thread.
@ -218,36 +236,21 @@ public sealed record ChatThread
//
// Add information from the profile if available and allowed:
// Add information from the profiles if available and allowed:
//
string systemPromptText;
logMessage = $"Using no profile for chat thread '{this.Name}'.";
if (string.IsNullOrWhiteSpace(this.SelectedProfile) || !this.allowProfile)
logMessage = $"Using no profiles for chat thread '{this.Name}'.";
var profiles = this.ResolveSelectedProfiles(settingsManager);
if (profiles.Count == 0 || !this.allowProfile)
systemPromptText = systemPromptWithAugmentedData;
else
{
if(!Guid.TryParse(this.SelectedProfile, out var profileId))
systemPromptText = systemPromptWithAugmentedData;
else
{
if(this.SelectedProfile == Profile.NO_PROFILE.Id || profileId == Guid.Empty)
systemPromptText = systemPromptWithAugmentedData;
else
{
var profile = settingsManager.GetProfileById(this.SelectedProfile);
if(profile == Profile.NO_PROFILE)
systemPromptText = systemPromptWithAugmentedData;
else
{
logMessage = $"Using profile '{profile.Name}' for chat thread '{this.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
logMessage = $"Using profiles '{string.Join("', '", profiles.Select(profile => profile.Name))}' for chat thread '{this.Name}'.";
systemPromptText = $"""
{systemPromptWithAugmentedData}
{profile.ToSystemPrompt()}
""";
}
}
}
{Profile.ToSystemPrompt(profiles)}
""";
}
LOGGER.LogInformation(logMessage);
@ -282,6 +285,14 @@ public sealed record ChatThread
""";
}
private IReadOnlyList<Profile> ResolveSelectedProfiles(SettingsManager settingsManager)
{
var profiles = settingsManager.ResolveProfiles(this.SelectedProfileIds);
var resolvedIds = profiles.Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.SelectedProfileIds.RemoveWhere(profileId => !resolvedIds.Contains(profileId));
return profiles;
}
/// <summary>
/// Removes a content block from this chat thread.
/// </summary>

View File

@ -123,7 +123,7 @@
<MudDivider Vertical="true" Style="height: 24px; align-self: center;"/>
<ProfileSelection MarginLeft="" CurrentProfile="@this.currentProfile" CurrentProfileChanged="@this.ProfileWasChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
<ProfileSelection MarginLeft="" SelectedProfileIds="@this.currentProfileIds" SelectedProfileIdsChanged="@this.ProfilesWereChanged" Disabled="@(!this.currentChatTemplate.AllowProfileUsage)" DisabledText="@T("Profile usage is disabled according to your chat template settings.")"/>
@if (this.SettingsManager.AreToolsEnabled())
{

View File

@ -70,7 +70,7 @@ public partial class ChatComponent : MSGComponentBase
private DataSourceSelection? dataSourceSelectionComponent;
private DataSourceOptions earlyDataSourceOptions = new();
private DataSourceOptions lastAppliedStandardDataSourceOptions = new();
private Profile currentProfile = Profile.NO_PROFILE;
private HashSet<string> currentProfileIds = [];
private ChatTemplate currentChatTemplate = ChatTemplate.NO_CHAT_TEMPLATE;
private bool hasUnsavedChanges;
private bool mustScrollToBottomAfterRender;
@ -127,7 +127,7 @@ public partial class ChatComponent : MSGComponentBase
USER_INPUT_ATTRIBUTES["id"] = CHAT_INPUT_ID;
// Get the preselected profile:
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
this.currentProfileIds = this.SettingsManager.GetPreselectedProfiles(Tools.Components.CHAT).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
// Get the preselected chat template:
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
@ -586,15 +586,15 @@ public partial class ChatComponent : MSGComponentBase
return threadName;
}
private async Task ProfileWasChanged(Profile profile)
private async Task ProfilesWereChanged(HashSet<string> profileIds)
{
this.currentProfile = this.SettingsManager.GetProfileById(profile.Id);
this.currentProfileIds = this.SettingsManager.ResolveProfiles(profileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
if(this.ChatThread is null)
return;
this.ChatThread = this.ChatThread with
{
SelectedProfile = this.currentProfile.Id,
SelectedProfileIds = [..this.currentProfileIds],
};
await this.ChatThreadChanged.InvokeAsync(this.ChatThread);
@ -617,7 +617,7 @@ public partial class ChatComponent : MSGComponentBase
private void RefreshCurrentProfileAndChatTemplate()
{
this.currentProfile = this.SettingsManager.GetProfileById(this.currentProfile.Id);
this.currentProfileIds = this.SettingsManager.ResolveProfiles(this.currentProfileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.currentChatTemplate = this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id);
}
@ -632,14 +632,12 @@ public partial class ChatComponent : MSGComponentBase
if (this.ChatThread is null)
{
this.currentProfile = this.SettingsManager.GetPreselectedProfile(Tools.Components.CHAT);
this.currentProfileIds = this.SettingsManager.GetPreselectedProfiles(Tools.Components.CHAT).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.currentChatTemplate = this.SettingsManager.GetPreselectedChatTemplate(Tools.Components.CHAT);
}
else
{
this.currentProfile = string.IsNullOrWhiteSpace(this.ChatThread.SelectedProfile)
? this.SettingsManager.GetProfileById(this.currentProfile.Id)
: this.SettingsManager.GetProfileById(this.ChatThread.SelectedProfile);
this.currentProfileIds = this.SettingsManager.ResolveProfiles(this.ChatThread.SelectedProfileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.currentChatTemplate = string.IsNullOrWhiteSpace(this.ChatThread.SelectedChatTemplate)
? this.SettingsManager.GetChatTemplateById(this.currentChatTemplate.Id)
@ -761,7 +759,7 @@ public partial class ChatComponent : MSGComponentBase
{
IncludeDateTime = true,
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SelectedProfileIds = [..this.currentProfileIds],
SelectedChatTemplate = this.currentChatTemplate.Id,
SelectedToolIds = [..this.selectedToolIds],
SystemPrompt = SystemPrompts.DEFAULT,
@ -806,7 +804,7 @@ public partial class ChatComponent : MSGComponentBase
{
IncludeDateTime = true,
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SelectedProfileIds = [..this.currentProfileIds],
SelectedChatTemplate = this.currentChatTemplate.Id,
SelectedToolIds = [..this.selectedToolIds],
SystemPrompt = SystemPrompts.DEFAULT,
@ -828,7 +826,7 @@ public partial class ChatComponent : MSGComponentBase
// Update provider, profile and chat template:
this.ChatThread.SelectedProvider = this.Provider.Id;
this.ChatThread.SelectedProfile = this.currentProfile.Id;
this.ChatThread.SelectedProfileIds = [..this.currentProfileIds];
//
// Remark: We do not update the chat template here
@ -1073,7 +1071,7 @@ public partial class ChatComponent : MSGComponentBase
{
IncludeDateTime = true,
SelectedProvider = this.Provider.Id,
SelectedProfile = this.currentProfile.Id,
SelectedProfileIds = [..this.currentProfileIds],
SelectedChatTemplate = this.currentChatTemplate.Id,
SelectedToolIds = [..this.selectedToolIds],
SystemPrompt = SystemPrompts.DEFAULT,
@ -1201,7 +1199,7 @@ public partial class ChatComponent : MSGComponentBase
private async Task SelectProviderWhenLoadingChat()
{
var chatProvider = this.ChatThread?.SelectedProvider;
var chatProfile = this.ChatThread?.SelectedProfile;
var chatProfileIds = this.ChatThread?.SelectedProfileIds;
var chatChatTemplate = this.ChatThread?.SelectedChatTemplate;
this.Provider = this.SettingsManager.GetChatProviderForLoadedChat(chatProvider);
@ -1209,8 +1207,8 @@ public partial class ChatComponent : MSGComponentBase
await this.ProviderChanged.InvokeAsync(this.Provider);
// Try to select the profile:
if (!string.IsNullOrWhiteSpace(chatProfile))
this.currentProfile = this.SettingsManager.GetProfileById(chatProfile);
if (chatProfileIds is not null)
this.currentProfileIds = this.SettingsManager.ResolveProfiles(chatProfileIds).Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase);
// Try to select the chat template:
if (!string.IsNullOrWhiteSpace(chatChatTemplate))

View File

@ -7,6 +7,7 @@
MultiSelectionTextFunc="@this.GetMultiSelectionText"
SelectedValues="@this.SelectedValues()"
Strict="@true"
MultiSelectionComponent="MultiSelectionComponent.CheckBox"
Disabled="@this.IsDisabled"
Margin="Margin.Dense"
Class="rounded-lg"
@ -23,13 +24,24 @@
<MudTooltip Text="@this.LockedTooltip()" Arrow="true" Placement="Placement.Right" RootStyle="display:inline-flex;">
<MudIcon Icon="@Icons.Material.Filled.Lock" Color="Color.Error" Size="Size.Small" Class="mr-1"/>
</MudTooltip>
@data.Name
@if (this.ItemTemplate is null)
{
@data.Name
}
else
{
@this.ItemTemplate(data)
}
</MudStack>
}
else if (this.ItemTemplate is not null)
{
@this.ItemTemplate(data)
}
else
{
@data.Name
}
</MudSelectItemExtended>
}
</MudSelectExtended>
</MudSelectExtended>

View File

@ -40,6 +40,15 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
[Parameter]
public Func<TData, bool> IsItemLocked { get; set; } = _ => false;
/// <summary>
/// Optional template used to render an item in the list.
/// </summary>
[Parameter]
public RenderFragment<ConfigurationSelectData<TData>>? ItemTemplate { get; set; }
[Parameter]
public Func<List<TData?>?, string>? MultiSelectionTextFunc { get; set; }
[Parameter]
public string? EmptySelectionText { get; set; }
@ -76,6 +85,9 @@ public partial class ConfigurationMultiSelect<TData> : ConfigurationBaseCore
private string GetMultiSelectionText(List<TData?>? selectedValues)
{
if (this.MultiSelectionTextFunc is not null)
return this.MultiSelectionTextFunc(selectedValues);
if(selectedValues is null || selectedValues.Count == 0)
return this.EmptySelectionText ?? T("No items selected.");

View File

@ -1,3 +1,4 @@
@using AIStudio.Settings
@inherits MSGComponentBase
@if (this.availableWorkspaces.Count > 0)
@ -20,14 +21,20 @@
</MudSelectItem>
}
</MudSelect>
<MudSelect T="string" Value="@this.ProfileId" ValueChanged="@this.SetProfileId" Label="@T("Chat profile")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no profile")</MudSelectItem>
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
}
<MudSelect T="ProfilePreselectionMode" Value="@this.SelectedProfileMode" ValueChanged="@this.SetProfileMode" Label="@T("Chat profiles")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
<MudSelectItem Value="@ProfilePreselectionMode.USE_APP_DEFAULT">@T("Use chat defaults")</MudSelectItem>
<MudSelectItem Value="@ProfilePreselectionMode.USE_NO_PROFILES">@T("Use no profiles")</MudSelectItem>
<MudSelectItem Value="@ProfilePreselectionMode.USE_SPECIFIC_PROFILES">@T("Use a custom profile selection")</MudSelectItem>
</MudSelect>
@if (this.SelectedProfileMode is ProfilePreselectionMode.USE_SPECIFIC_PROFILES)
{
<MudSelect T="string" MultiSelection="true" SelectedValues="@(this.ProfileIds ?? [])" SelectedValuesChanged="@this.SetProfileIds" MultiSelectionTextFunc="@this.GetSelectedProfileText" Label="@T("Profiles")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudSelectItem T="string" Value="@profile.Id">@profile.GetSafeName()</MudSelectItem>
}
</MudSelect>
}
<MudSelect T="string" Value="@this.ChatTemplateId" ValueChanged="@this.SetChatTemplateId" Label="@T("Chat template")" Variant="Variant.Outlined" Margin="Margin.Dense" Class="mb-3 rounded-lg" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Article">
<MudSelectItem T="string" Value="@string.Empty">@T("Use chat default")</MudSelectItem>
<MudSelectItem T="string" Value="@Guid.Empty.ToString()">@T("Use no chat template")</MudSelectItem>
@ -43,4 +50,4 @@
}
</MudSelect>
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>
<ToolSelectionField Component="Components.CHAT" SelectedToolIds="@this.ToolIds" SelectedToolIdsChanged="@this.SetToolIds" Label="@T("Tools (Optional)")" Help="@T("These tools are preselected when the chat opens. Users can change the selection in the chat, and every tool has to meet the confidence requirements of the provider in use.")"/>

View File

@ -1,3 +1,5 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
@ -33,14 +35,13 @@ public partial class DirectChatLauncherForm : MSGComponentBase
public EventCallback<string> ProviderIdChanged { get; set; }
/// <summary>
/// The profile ID for the chat, an empty GUID for explicitly no profile, or an empty string to
/// use the chat default.
/// The exact profile IDs for the chat, an empty set for no profiles, or null for chat defaults.
/// </summary>
[Parameter]
public string ProfileId { get; set; } = string.Empty;
public HashSet<string>? ProfileIds { get; set; }
[Parameter]
public EventCallback<string> ProfileIdChanged { get; set; }
public EventCallback<HashSet<string>?> ProfileIdsChanged { get; set; }
/// <summary>
/// The chat template ID, an empty GUID for explicitly no template, or an empty string to use
@ -82,6 +83,7 @@ public partial class DirectChatLauncherForm : MSGComponentBase
public Func<string, string?>? ValidateWorkspaceName { get; set; }
private IReadOnlyList<WorkspaceTreeWorkspace> availableWorkspaces = [];
private ProfilePreselectionMode? profileModeOverride;
private static readonly Dictionary<string, object?> USER_INPUT_ATTRIBUTES = new();
@ -124,10 +126,30 @@ public partial class DirectChatLauncherForm : MSGComponentBase
await this.ProviderIdChanged.InvokeAsync(providerId);
}
private async Task SetProfileId(string profileId)
private ProfilePreselectionMode SelectedProfileMode => this.profileModeOverride ?? ProfilePreselection.FromStoredValue(this.ProfileIds).Mode;
private async Task SetProfileMode(ProfilePreselectionMode mode)
{
this.ProfileId = profileId;
await this.ProfileIdChanged.InvokeAsync(profileId);
this.profileModeOverride = mode;
HashSet<string>? profileIds = mode switch
{
ProfilePreselectionMode.USE_APP_DEFAULT => null,
ProfilePreselectionMode.USE_NO_PROFILES => [],
ProfilePreselectionMode.USE_SPECIFIC_PROFILES => this.ProfileIds is { Count: > 0 } ? [..this.ProfileIds] : [],
_ => null,
};
this.ProfileIds = profileIds;
await this.ProfileIdsChanged.InvokeAsync(profileIds);
}
private async Task SetProfileIds(IEnumerable<string?>? profileIds)
{
var selection = profileIds is null
? []
: profileIds.Where(profileId => !string.IsNullOrWhiteSpace(profileId)).Select(profileId => profileId!).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.ProfileIds = selection;
await this.ProfileIdsChanged.InvokeAsync(selection);
}
private async Task SetChatTemplateId(string chatTemplateId)
@ -161,4 +183,15 @@ public partial class DirectChatLauncherForm : MSGComponentBase
return string.Format(T("{0} data source(s) selected"), selectedValues.Count);
}
}
private string GetSelectedProfileText(List<string?>? selectedValues)
{
var count = selectedValues?.Count ?? 0;
return count switch
{
0 => T("No profiles selected"),
1 => T("1 profile"),
_ => string.Format(T("{0} profiles"), count),
};
}
}

View File

@ -2,10 +2,10 @@
@inherits MSGComponentBase
<MudStack Row="true" AlignItems="AlignItems.Baseline" StretchItems="StretchItems.Start" Class="mb-3" Wrap="Wrap.NoWrap">
<MudSelect T="Profile" Strict="@true" Disabled="@this.Disabled" Value="@this.Profile" ValueChanged="@this.SelectionChanged" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person4" Margin="Margin.Dense" Label="@T("Select one of your profiles")" Variant="Variant.Outlined" Class="mb-3" Validation="@this.Validation">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
<MudSelect T="string" MultiSelection="@true" Strict="@true" Disabled="@this.Disabled" SelectedValues="@this.ProfileIds" SelectedValuesChanged="@this.SelectionChanged" MultiSelectionTextFunc="@this.GetSelectionText" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Person4" Margin="Margin.Dense" Label="@T("Select your profiles")" Variant="Variant.Outlined" Class="mb-3" Validation="@this.ValidateSelection">
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudSelectItem Value="profile">
<MudSelectItem T="string" Value="@profile.Id">
@profile.GetSafeName()
</MudSelectItem>
}

View File

@ -10,13 +10,13 @@ namespace AIStudio.Components;
public partial class ProfileFormSelection : MSGComponentBase
{
[Parameter]
public Profile Profile { get; set; } = Profile.NO_PROFILE;
public HashSet<string> ProfileIds { get; set; } = [];
[Parameter]
public EventCallback<Profile> ProfileChanged { get; set; }
public EventCallback<HashSet<string>> ProfileIdsChanged { get; set; }
[Parameter]
public Func<Profile, string?> Validation { get; set; } = _ => null;
public Func<HashSet<string>, string?> Validation { get; set; } = _ => null;
[Parameter]
public bool Disabled { get; set; }
@ -24,10 +24,29 @@ public partial class ProfileFormSelection : MSGComponentBase
[Inject]
public IDialogService DialogService { get; init; } = null!;
private async Task SelectionChanged(Profile profile)
private async Task SelectionChanged(IEnumerable<string?>? profileIds)
{
this.Profile = profile;
await this.ProfileChanged.InvokeAsync(profile);
var selection = profileIds is null
? []
: profileIds.Where(profileId => !string.IsNullOrWhiteSpace(profileId)).Select(profileId => profileId!).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.ProfileIds = selection;
await this.ProfileIdsChanged.InvokeAsync(selection);
}
private string? ValidateSelection(IEnumerable<string?>? profileIds) => this.Validation(
profileIds is null
? []
: profileIds.Where(profileId => !string.IsNullOrWhiteSpace(profileId)).Select(profileId => profileId!).ToHashSet(StringComparer.OrdinalIgnoreCase));
private string GetSelectionText(List<string?>? profileIds)
{
var profiles = this.SettingsManager.ResolveProfiles(profileIds?.Where(profileId => profileId is not null).Select(profileId => profileId!));
return profiles.Count switch
{
0 => T("No profiles selected"),
1 => profiles[0].GetSafeName(),
_ => string.Format(T("{0} profiles"), profiles.Count),
};
}
private async Task OpenSettingsDialog()
@ -35,4 +54,4 @@ public partial class ProfileFormSelection : MSGComponentBase
var dialogParameters = new DialogParameters();
await this.DialogService.ShowAsync<SettingsDialogProfiles>(T("Open Profile Options"), dialogParameters, DialogOptions.FULLSCREEN);
}
}
}

View File

@ -0,0 +1,16 @@
@using AIStudio.Settings
@inherits MSGComponentBase
<ConfigurationSelect TConfig="ProfilePreselectionMode" OptionDescription="@this.OptionDescription" OptionHelp="@this.OptionHelp" Disabled="@this.Disabled" IsLocked="@this.IsLocked" SelectedValue="@(() => this.SelectedMode)" Data="@this.GetModeData()" SelectionUpdateAsync="@this.ModeChanged" />
@if (this.SelectedMode is ProfilePreselectionMode.USE_SPECIFIC_PROFILES)
{
<ConfigurationMultiSelect TData="string" OptionDescription="@T("Profiles")" Disabled="@this.Disabled" IsLocked="@this.IsLocked" SelectedValues="@(() => this.SpecificProfileIds)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdateAsync="@this.ProfilesChanged" MultiSelectionTextFunc="@this.GetSelectionText">
<ItemTemplate Context="profileData">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@this.ProfileIcon(profileData.Value)" Size="Size.Small" Style="color: var(--mud-palette-action-default);" />
<MudText Typo="Typo.body1">@profileData.Name</MudText>
</MudStack>
</ItemTemplate>
</ConfigurationMultiSelect>
}

View File

@ -0,0 +1,75 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components;
public partial class ProfilePreselectionConfiguration : MSGComponentBase
{
private ProfilePreselectionMode? selectedModeOverride;
[Parameter]
public string OptionDescription { get; set; } = string.Empty;
[Parameter]
public string OptionHelp { get; set; } = string.Empty;
[Parameter]
public Func<bool> Disabled { get; set; } = () => false;
[Parameter]
public Func<bool> IsLocked { get; set; } = () => false;
[Parameter]
public Func<HashSet<string>?> SelectedProfileIds { get; set; } = () => null;
[Parameter]
public Action<HashSet<string>?> SelectionUpdate { get; set; } = _ => { };
[Parameter]
public Func<HashSet<string>?, Task> SelectionUpdateAsync { get; set; } = _ => Task.CompletedTask;
private ProfilePreselectionMode SelectedMode => this.selectedModeOverride ?? ProfilePreselection.FromStoredValue(this.SelectedProfileIds()).Mode;
private HashSet<string> SpecificProfileIds => this.SelectedProfileIds() ?? [];
private async Task ModeChanged(ProfilePreselectionMode mode)
{
this.selectedModeOverride = mode;
HashSet<string>? selection = mode switch
{
ProfilePreselectionMode.USE_APP_DEFAULT => null,
ProfilePreselectionMode.USE_NO_PROFILES => [],
ProfilePreselectionMode.USE_SPECIFIC_PROFILES => this.SpecificProfileIds.Count > 0 ? [..this.SpecificProfileIds] : [],
_ => null,
};
await this.UpdateSelection(selection);
await this.InvokeAsync(this.StateHasChanged);
}
private async Task ProfilesChanged(HashSet<string> profileIds) => await this.UpdateSelection(profileIds.ToHashSet(StringComparer.OrdinalIgnoreCase));
private async Task UpdateSelection(HashSet<string>? selection)
{
this.SelectionUpdate(selection);
await this.SelectionUpdateAsync(selection);
}
private string GetSelectionText(List<string?>? profileIds)
{
var profiles = this.SettingsManager.ResolveProfiles(profileIds?.OfType<string>());
return profiles.Count == 0 ? T("No profiles selected") : string.Join(", ", profiles.Select(profile => profile.GetSafeName()));
}
private IEnumerable<ConfigurationSelectData<ProfilePreselectionMode>> GetModeData()
{
yield return new(T("Use app default"), ProfilePreselectionMode.USE_APP_DEFAULT);
yield return new(T("Use no profiles"), ProfilePreselectionMode.USE_NO_PROFILES);
yield return new(T("Use a custom profile selection"), ProfilePreselectionMode.USE_SPECIFIC_PROFILES);
}
private string ProfileIcon(string profileId) => this.SettingsManager.GetProfileById(profileId).IsEnterpriseConfiguration
? Icons.Material.Filled.Business
: Icons.Material.Filled.Person4;
}

View File

@ -1,12 +1,12 @@
@using AIStudio.Settings
@inherits MSGComponentBase
<MudTooltip Text="@this.ToolTipText" Placement="Placement.Top">
<MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.CurrentProfile.Name" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass" Disabled="@this.Disabled">
<MudMenu TransformOrigin="@Origin.BottomLeft" AnchorOrigin="Origin.TopLeft" StartIcon="@Icons.Material.Filled.Person4" EndIcon="@Icons.Material.Filled.KeyboardArrowDown" Label="@this.SelectionLabel" Variant="Variant.Filled" Color="Color.Default" Class="@this.MarginClass" Disabled="@this.Disabled">
<ActivatorContent>
@if (this.CurrentProfile != Profile.NO_PROFILE)
@if (this.SelectedProfiles.Count > 0)
{
<MudButton IconSize="Size.Large" StartIcon="@Icons.Material.Filled.Person4" IconColor="Color.Default">
@this.CurrentProfile.GetSafeName()
@this.SelectionLabel
</MudButton>
}
else
@ -17,10 +17,16 @@
<ChildContent>
<MudMenuItem Icon="@Icons.Material.Filled.Settings" Label="@T("Manage your profiles")" OnClick="@(async () => await this.OpenSettingsDialog())" />
<MudDivider/>
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles.GetAllProfiles())
<MudMenuItem Icon="@Icons.Material.Filled.PersonOff" AutoClose="false" Disabled="@(this.SelectedProfiles.Count == 0)" OnClick="@this.ClearSelection">
@T("Clear all")
</MudMenuItem>
@foreach (var profile in this.SettingsManager.ConfigurationData.Profiles)
{
<MudMenuItem Icon="@this.ProfileIcon(profile)" OnClick="@(() => this.SelectionChanged(profile))">
@profile.GetSafeName()
<MudMenuItem Icon="@(this.SelectedProfileIds.Contains(profile.Id) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank)" IconColor="Color.Primary" AutoClose="false" Class="mud-list-item-dense" OnClick="@(() => this.SelectionChanged(profile, !this.SelectedProfileIds.Contains(profile.Id)))">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@this.ProfileIcon(profile)" Size="Size.Small" Style="color: var(--mud-palette-action-default);" />
<MudText Typo="Typo.body1">@profile.GetSafeName()</MudText>
</MudStack>
</MudMenuItem>
}
</ChildContent>

View File

@ -12,10 +12,10 @@ public partial class ProfileSelection : MSGComponentBase
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProfileSelection).Namespace, nameof(ProfileSelection));
[Parameter]
public Profile CurrentProfile { get; set; } = Profile.NO_PROFILE;
public HashSet<string> SelectedProfileIds { get; set; } = [];
[Parameter]
public EventCallback<Profile> CurrentProfileChanged { get; set; }
public EventCallback<HashSet<string>> SelectedProfileIdsChanged { get; set; }
[Parameter]
public string MarginLeft { get; set; } = "ml-3";
@ -32,9 +32,22 @@ public partial class ProfileSelection : MSGComponentBase
[Inject]
private IDialogService DialogService { get; init; } = null!;
private readonly string defaultToolTipText = TB("You can switch between your profiles here");
private readonly string defaultToolTipText = TB("You can select your profiles here");
private string ToolTipText => this.Disabled ? this.DisabledText : this.defaultToolTipText;
private IReadOnlyList<Profile> SelectedProfiles => this.SettingsManager.ResolveProfiles(this.SelectedProfileIds);
private string SelectionLabel => this.SelectedProfiles.Count switch
{
0 => string.Empty,
1 => this.SelectedProfiles[0].GetSafeName(),
_ => string.Format(TB("{0} profiles"), this.SelectedProfiles.Count),
};
private string ToolTipText => this.Disabled
? this.DisabledText
: this.SelectedProfiles.Count > 1
? string.Join(", ", this.SelectedProfiles.Select(profile => profile.GetSafeName()))
: this.defaultToolTipText;
private string MarginClass => $"{this.MarginLeft} {this.MarginRight}";
@ -47,19 +60,31 @@ public partial class ProfileSelection : MSGComponentBase
}
#endregion
private string ProfileIcon(Profile profile)
{
if (profile.IsEnterpriseConfiguration)
return Icons.Material.Filled.Business;
return Icons.Material.Filled.Person4;
}
private async Task SelectionChanged(Profile profile)
private async Task SelectionChanged(Profile profile, bool selected)
{
this.CurrentProfile = profile;
await this.CurrentProfileChanged.InvokeAsync(profile);
var updatedSelection = new HashSet<string>(this.SelectedProfileIds, StringComparer.OrdinalIgnoreCase);
if (selected)
updatedSelection.Add(profile.Id);
else
updatedSelection.Remove(profile.Id);
this.SelectedProfileIds = updatedSelection;
await this.SelectedProfileIdsChanged.InvokeAsync(updatedSelection);
}
private async Task ClearSelection()
{
this.SelectedProfileIds = [];
await this.SelectedProfileIdsChanged.InvokeAsync([]);
}
private async Task OpenSettingsDialog()
@ -79,4 +104,4 @@ public partial class ProfileSelection : MSGComponentBase
}
#endregion
}
}

View File

@ -33,7 +33,7 @@
}
<ConfigurationProviderSelection Component="Components.APP_SETTINGS" Data="@this.AvailableLLMProvidersFunc()" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProvider = selectedValue)" HelpText="@(() => T("Would you like to set one provider as the default for the entire app? When you configure a different provider for an assistant, it will always take precedence."))" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your profiles?")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfile)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfile = selectedValue)" OptionHelp="@T("Would you like to set one of your profiles as the default for the entire app? When you configure a different profile for an assistant, it will always take precedence.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
<ConfigurationMultiSelect OptionDescription="@T("Preselect profiles")" SelectedValues="@(() => this.SettingsManager.ConfigurationData.App.PreselectedProfileIds)" Data="@ConfigurationSelectDataFactory.GetProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Choose the profiles used by default throughout the app. A component-specific selection replaces this set completely.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.PreselectedProfileIds, out var meta) && meta.IsLocked" EmptySelectionText="@T("No profiles selected.")" SingleSelectionText="@T("You have selected 1 profile.")" MultipleSelectionText="@T("You have selected {0} profiles.")"/>
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
{

View File

@ -29,7 +29,7 @@
<MudTextField T="string" @bind-Text="@this.description" Validation="@this.ValidateDescription" AdornmentIcon="@Icons.Material.Filled.Notes" Adornment="Adornment.Start" IconSize="Size.Small" Label="@T("Description")" HelperText="@T("Shown on the tile and on the plugins page.")" Variant="Variant.Outlined" Margin="Margin.Dense" Lines="2" AutoGrow="@true" MaxLines="6" Class="mb-3" Disabled="@this.IsBusy"/>
<DirectChatLauncherForm @bind-WorkspaceName="@this.workspaceName"
@bind-ProviderId="@this.providerId"
@bind-ProfileId="@this.profileId"
@bind-ProfileIds="@this.profileIds"
@bind-ChatTemplateId="@this.chatTemplateId"
@bind-DataSourceIds="@this.dataSourceIds"
@bind-ToolIds="@this.toolIds"
@ -78,4 +78,4 @@
@T("Save tile")
</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -45,7 +45,7 @@ public partial class DirectChatLauncherSettingsDialog : MSGComponentBase
private string description = string.Empty;
private string workspaceName = string.Empty;
private string providerId = string.Empty;
private string profileId = string.Empty;
private HashSet<string>? profileIds;
private string chatTemplateId = string.Empty;
private IEnumerable<string> dataSourceIds = [];
private HashSet<string> toolIds = [];
@ -112,7 +112,7 @@ public partial class DirectChatLauncherSettingsDialog : MSGComponentBase
this.workspaceName = launch.WorkspaceName;
this.providerId = launch.ProviderId?.ToString() ?? string.Empty;
this.profileId = launch.ProfileId?.ToString() ?? string.Empty;
this.profileIds = launch.ProfileIds is null ? null : launch.ProfileIds.Select(id => id.ToString()).ToHashSet(StringComparer.OrdinalIgnoreCase);
this.chatTemplateId = launch.ChatTemplateId?.ToString() ?? string.Empty;
this.dataSourceIds = launch.DataSourceIds?.Select(id => id.ToString()).ToArray() ?? [];
this.toolIds = launch.ToolIds is null ? [] : [..launch.ToolIds];
@ -157,7 +157,7 @@ public partial class DirectChatLauncherSettingsDialog : MSGComponentBase
return new(
this.workspaceName.Trim(),
ParseOptionalGuid(this.providerId),
ParseOptionalGuid(this.profileId),
this.profileIds?.Select(Guid.Parse).Order().ToArray(),
ParseOptionalGuid(this.chatTemplateId),
selectedDataSourceIds.Length == 0 ? null : selectedDataSourceIds,
this.toolIds.Count == 0 ? null : this.toolIds.Order(StringComparer.Ordinal).ToArray());
@ -277,4 +277,4 @@ public partial class DirectChatLauncherSettingsDialog : MSGComponentBase
Path.GetFullPath(right).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar),
comparison);
}
}
}

View File

@ -34,7 +34,7 @@
}
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.AGENDA_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Agenda.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Agenda.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.AGENDA_ASSISTANT" />
</DialogContent>

View File

@ -27,7 +27,7 @@
{
<ConfigurationText OptionDescription="@T("Preselect another language")" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedOtherLanguage = updatedText)"/>
}
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.BIAS_DAY_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider = selectedValue)"/>
</MudPaper>

View File

@ -17,7 +17,7 @@
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="@T("Preselect chat options?")" LabelOn="@T("Chat options are preselected")" LabelOff="@T("No chat options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Chat.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect chat options. This is might be useful when you prefer a specific provider.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectOptions, out var meta) && meta.IsLocked"/>
<ConfigurationProviderSelection Component="Components.CHAT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProvider = selectedValue)" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProvider, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Chat.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether chats should use the app default profile, no profile, or a specific profile.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedProfile, out var meta) && meta.IsLocked"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")" IsLocked="() => ManagedConfiguration.TryGetProfilePreselection(x => x.Chat, x => x.PreselectedProfileIds, out var meta) && meta.IsLocked"/>
<ConfigurationSelect OptionDescription="@T("Preselect one of your chat templates?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Chat.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate)" Data="@ConfigurationSelectDataFactory.GetChatTemplatesData(this.SettingsManager.ConfigurationData.ChatTemplates)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Chat.PreselectedChatTemplate = selectedValue)" OptionHelp="@T("Would you like to set one of your chat templates as the default for chats?")" IsLocked="() => ManagedConfiguration.TryGet(x => x.Chat, x => x.PreselectedChatTemplate, out var meta) && meta.IsLocked"/>
</MudPaper>

View File

@ -14,7 +14,7 @@
<ConfigurationOption OptionDescription="@T("Preselect compiler messages?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" LabelOn="@T("Compiler messages are preselected")" LabelOff="@T("Compiler messages are not preselected")" State="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.Coding.PreselectCompilerMessages = updatedState)" />
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.CODING_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.Coding.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.Coding.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.Coding.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.Coding.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.CODING_ASSISTANT" IncludeVisibilityToggle="@false" />
</DialogContent>

View File

@ -13,7 +13,7 @@
<MudPaper Class="pa-3 mb-8 border-dashed border rounded-lg">
<ConfigurationOption OptionDescription="@T("Preselect ERI server options?")" LabelOn="@T("ERI server options are preselected")" LabelOff="@T("No ERI server options are preselected")" State="@(() => this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.ERI.PreselectOptions = updatedState)" OptionHelp="@T("When enabled, you can preselect some ERI server options.")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.ERI.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ERI.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.ERI.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ERI.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.ERI.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.ERI.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.ERI.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
<MudText Typo="Typo.body1" Class="mb-3">
@T("Most ERI server options can be customized and saved directly in the ERI server assistant. For this, the ERI server assistant has an auto-save function.")

View File

@ -15,7 +15,7 @@
<ConfigurationOption OptionDescription="@T("Preselect the content cleaner agent?")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions || this.SettingsManager.ConfigurationData.LegalCheck.HideWebContentReader)" LabelOn="@T("Content cleaner agent is preselected")" LabelOff="@T("Content cleaner agent is not preselected")" State="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent)" StateUpdate="@(updatedState => this.SettingsManager.ConfigurationData.LegalCheck.PreselectContentCleanerAgent = updatedState)" OptionHelp="@T("When enabled, the content cleaner agent is preselected. This is might be useful when you prefer to clean up the legal content before translating it.")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.LEGAL_CHECK_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.LegalCheck.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.LegalCheck.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.LEGAL_CHECK_ASSISTANT" />
</DialogContent>

View File

@ -16,7 +16,7 @@
{
<ConfigurationText OptionDescription="@T("Preselect another language")" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" Icon="@Icons.Material.Filled.Translate" Text="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectOtherLanguage)" TextUpdate="@(updatedText => this.SettingsManager.ConfigurationData.MyTasks.PreselectOtherLanguage = updatedText)"/>
}
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.MY_TASKS_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.MyTasks.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.MyTasks.PreselectedProvider = selectedValue)"/>
</MudPaper>

View File

@ -23,7 +23,7 @@
<ConfigurationSelect OptionDescription="@T("Preselect the audience expertise")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceExpertise)" Data="@ConfigurationSelectDataFactory.GetSlideBuilderAudienceExpertiseData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedAudienceExpertise = selectedValue)" OptionHelp="@T("Which audience expertise should be preselected?")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.SLIDE_BUILDER_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.SlideBuilder.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.SlideBuilder.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.SLIDE_BUILDER_ASSISTANT" IncludeVisibilityToggle="@false" />
</DialogContent>

View File

@ -23,10 +23,10 @@
<ConfigurationOption OptionDescription="@T("Optimize large visual assets by default?")" LabelOn="@T("Large visual assets are optimized")" LabelOff="@T("Visual assets keep their original size")" State="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages)" StateUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.OptimizeImages = value)"/>
<ConfigurationMinConfidenceSelection RestrictToGlobalMinimumConfidence="true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence = value)"/>
<ConfigurationProviderSelection Component="Components.VISUAL_BRIEFING_ASSISTANT" Data="@this.AvailableLLMProviders" SelectedValue="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProvider = value)"/>
<ConfigurationSelect OptionDescription="@T("Default profile")" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfile = value)"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Default profiles")" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfileIds)" SelectionUpdate="@(value => this.SettingsManager.ConfigurationData.VisualBriefing.PreselectedProfileIds = value)" IsLocked="() => ManagedConfiguration.TryGetProfilePreselection(x => x.VisualBriefing, x => x.PreselectedProfileIds, out var meta) && meta.IsLocked"/>
</MudPaper>
</DialogContent>
<DialogActions>
<MudButton OnClick="@this.Close" Variant="Variant.Filled">@T("Close")</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@ -21,7 +21,7 @@
<ConfigurationSelect OptionDescription="@T("Preselect a writing style")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle)" Data="@ConfigurationSelectDataFactory.GetWritingStyles4EMailData()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedWritingStyle = selectedValue)" OptionHelp="@T("Which writing style should be preselected?")"/>
<ConfigurationMinConfidenceSelection Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" RestrictToGlobalMinimumConfidence="@true" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.MinimumProviderConfidence)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.MinimumProviderConfidence = selectedValue)"/>
<ConfigurationProviderSelection Component="Components.EMAIL_ASSISTANT" Data="@this.AvailableLLMProviders" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProvider = selectedValue)"/>
<ConfigurationSelect OptionDescription="@T("Preselect a profile")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedValue="@(() => ProfilePreselection.FromStoredValue(this.SettingsManager.ConfigurationData.EMail.PreselectedProfile))" Data="@ConfigurationSelectDataFactory.GetComponentProfilesData(this.SettingsManager.ConfigurationData.Profiles)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfile = selectedValue)" OptionHelp="@T("Choose whether the assistant should use the app default profile, no profile, or a specific profile.")"/>
<ProfilePreselectionConfiguration OptionDescription="@T("Preselect profiles")" Disabled="@(() => !this.SettingsManager.ConfigurationData.EMail.PreselectOptions)" SelectedProfileIds="@(() => this.SettingsManager.ConfigurationData.EMail.PreselectedProfileIds)" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.EMail.PreselectedProfileIds = selectedValue)" OptionHelp="@T("Use the app default profiles, no profiles, or a custom selection that completely replaces the app default.")"/>
</MudPaper>
<ToolDefaultsConfiguration Component="Components.EMAIL_ASSISTANT" />
</DialogContent>

View File

@ -27,7 +27,7 @@ This folder keeps the Lua manifest (`plugin.lua`) that defines a custom assistan
- [Using component metadata inside BuildPrompt](#using-component-metadata-inside-buildprompt)
- [Example: build a prompt from two fields](#example-build-a-prompt-from-two-fields)
- [Example: reuse a label from `Props`](#example-reuse-a-label-from-props)
- [Using `profile` inside BuildPrompt](#using-profile-inside-buildprompt)
- [Using `profiles` inside BuildPrompt](#using-profiles-inside-buildprompt)
- [Example: Add user profile context to the prompt](#example-add-user-profile-context-to-the-prompt)
- [Advanced Layout Options](#advanced-layout-options)
- [`LAYOUT_GRID` reference](#layout_grid-reference)
@ -118,7 +118,9 @@ ASSISTANT = {
["LaunchBehavior"] = "OPEN_WORKSPACE_CHAT_BY_NAME",
["WorkspaceName"] = "XXX",
["ProviderId"] = "11111111-1111-1111-1111-111111111111", -- optional
["ProfileId"] = "22222222-2222-2222-2222-222222222222", -- optional
["ProfileIds"] = { -- optional; use an empty list for no profiles
"22222222-2222-2222-2222-222222222222",
},
["ChatTemplateId"] = "33333333-3333-3333-3333-333333333333", -- optional
["DataSourceIds"] = { -- optional; when present, at least one unique data source is required
"44444444-4444-4444-4444-444444444444",
@ -132,12 +134,13 @@ ASSISTANT = {
- `WorkspaceName` is resolved case-insensitively after trimming.
- If the workspace does not exist yet, AI Studio creates it automatically.
- Omitted optional IDs use the chat defaults active when the tile is opened. An explicit empty GUID selects no profile or no chat template; an empty provider or data-source GUID is invalid.
- Omitted optional fields use the chat defaults active when the tile is opened. `ProfileIds = {}` explicitly selects no profiles; a non-empty list selects exactly those unique profiles. An explicit empty chat-template GUID selects no template. Empty provider, profile, and data-source GUIDs are invalid.
- The legacy singular `ProfileId` field remains supported when used by itself, including an empty GUID for no profile. Supplying `ProfileId` and `ProfileIds` together is invalid.
- `ProviderId` overrides both the chat-specific and app-wide default provider. It must name a provider that is permitted for chats at the required confidence level.
- Explicit data sources are enabled and manually preselected, automatic source selection is disabled, and the normal automatic-validation setting is retained. Every referenced source must currently be available and permitted for the effective provider.
- Invalid or unavailable references stop the launch with an error before a workspace or chat is created.
- A selected chat template supplies the chat system prompt, profile allowance, predefined user prompt, attachments, and cloned example conversation. A launcher `SystemPrompt`, if retained in an older plugin, is ignored, so there is never a second competing system prompt.
- When the selected chat template does not allow profiles, the template wins: the launcher `ProfileId` is dropped and the chat starts without a profile. This matches the disabled profile selection such a template produces in the chat.
- When the selected chat template does not allow profiles, the template wins: the launcher `ProfileIds` are dropped and the chat starts without profiles. This matches the disabled profile selection such a template produces in the chat.
- The predefined user prompt and the attachments of the selected chat template are placed into the chat input, unless the user already has an unsent draft there.
### Editing a launcher in AI Studio
@ -702,10 +705,14 @@ The function receives a single `input` Lua table with:
- `Type` (string, e.g. `TEXT_AREA`, `DROPDOWN`, `SWITCH`, `COLOR_PICKER`, `DATE_PICKER`, `DATE_RANGE_PICKER`, `TIME_PICKER`)
- `Value` (current component value)
- `Props` (readable component props)
- `input.profile`: selected profile data
- `Name`, `NeedToKnow`, `Actions`, `Num`
- When no profile is selected, values match the built-in "Use no profile" entry
- `profile` is a reserved key in the input table
- `input.profiles`: an array containing every selected profile
- Each entry contains `Id`, `Name`, `NeedToKnow`, `Actions`, and `Num`
- The array is empty when no profiles are selected
- `profiles` is a reserved key in the input table
- `input.profile`: compatibility value available when zero or one profile is selected
- It contains the same profile table as the sole entry in `input.profiles`
- With no selection, its values match the built-in "Use no profile" entry
- The key is absent when multiple profiles are selected
```
input = {
["<Name>"] = {
@ -717,7 +724,17 @@ input = {
UserPrompt = "<string?>"
}
},
profile = {
profiles = {
{
Id = "<string>",
Name = "<string>",
NeedToKnow = "<string>",
Actions = "<string>",
Num = <number>
}
},
profile = { -- present when zero or one profile is selected
Id = "<string>",
Name = "<string>",
NeedToKnow = "<string>",
Actions = "<string>",
@ -804,17 +821,19 @@ return {
---
### Using `profile` inside BuildPrompt
Profiles are optional user context (e.g., "NeedToKnow" and "Actions"). You can inject this directly into the user prompt if you want the LLM to always see it.
### Using `profiles` inside BuildPrompt
Profiles are optional user context (e.g., "NeedToKnow" and "Actions"). Iterate over `input.profiles` to include all selected profiles. Use `input.profile` only for compatibility code that deliberately applies to a single selection.
#### Example: Add user profile context to the prompt
```lua
ASSISTANT.BuildPrompt = function(input)
local parts = {}
if input.profile and input.profile.NeedToKnow ~= "" then
table.insert(parts, "User context:")
table.insert(parts, input.profile.NeedToKnow)
table.insert(parts, "")
for _, profile in ipairs(input.profiles or {}) do
if profile.NeedToKnow ~= "" then
table.insert(parts, "User context for " .. profile.Name .. ":")
table.insert(parts, profile.NeedToKnow)
table.insert(parts, "")
end
end
table.insert(parts, input.Main and input.Main.Value or "")
return table.concat(parts, "\n")

View File

@ -451,7 +451,9 @@ ASSISTANT = {
["LaunchBehavior"] = "OPEN_WORKSPACE_CHAT_BY_NAME",
["WorkspaceName"] = "<name of the workspace to open or create>",
["ProviderId"] = "<optional provider GUID; omit to use the chat default>",
["ProfileId"] = "<optional profile GUID; use the empty GUID for no profile>",
["ProfileIds"] = {
"<optional unique profile GUID; use an empty list for no profiles>",
},
["ChatTemplateId"] = "<optional chat template GUID; use the empty GUID for no template>",
["DataSourceIds"] = {
"<optional data source GUID>",

View File

@ -403,10 +403,14 @@ CONFIG["SETTINGS"] = {}
-- Please note: using an empty string ("") will lock the preselected provider selection, even though no valid preselected provider is found.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
-- Configure the preselected profile.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- Please note: using an empty string ("") will lock the preselected profile selection, even though no valid preselected profile is found.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
-- Configure the app-wide preselected profiles.
-- Every entry must be a unique, non-empty profile ID defined in CONFIG["PROFILES"].
-- An empty list means that no profiles are preselected.
-- CONFIG["SETTINGS"]["DataApp.PreselectedProfileIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- }
-- The legacy singular DataApp.PreselectedProfile setting remains supported by itself.
-- Supplying the singular and plural setting together is invalid.
-- Configure chat-specific preselected options.
-- This must be enabled for the chat-specific provider, profile, and chat template to take effect.
@ -416,11 +420,15 @@ CONFIG["SETTINGS"] = {}
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected profile for chats.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- Please note: using an empty string ("") means chats will use the app default profile.
-- Please note: using "00000000-0000-0000-0000-000000000000" means chats will use no profile.
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
-- Configure the preselected profiles for chats.
-- Omit this setting to use the app-wide selection. An empty list means no profiles.
-- A non-empty list replaces the app-wide selection; every entry must be a unique,
-- non-empty profile ID defined in CONFIG["PROFILES"].
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfileIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- }
-- The legacy singular DataChat.PreselectedProfile setting remains supported by itself.
-- Supplying the singular and plural setting together is invalid.
--
-- Configure the preselected chat template for chats.
-- It must be one of the chat template IDs defined in CONFIG["CHAT_TEMPLATES"].
@ -458,7 +466,7 @@ CONFIG["SETTINGS"] = {}
-- Allowed values are: true, false
-- CONFIG["SETTINGS"]["DataChat.PreselectOptions.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProvider.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfile.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedProfileIds.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedChatTemplate.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourcesDisabled.AllowUserOverride"] = true
-- CONFIG["SETTINGS"]["DataChat.PreselectedDataSourcesAutomaticSelection.AllowUserOverride"] = true
@ -569,9 +577,15 @@ CONFIG["SETTINGS"] = {}
-- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"].
-- CONFIG["SETTINGS"]["DataVisualBriefing.PreselectedProvider"] = "00000000-0000-0000-0000-000000000000"
--
-- Configure the preselected profile for briefing builds.
-- It must be one of the profile IDs defined in CONFIG["PROFILES"].
-- CONFIG["SETTINGS"]["DataVisualBriefing.PreselectedProfile"] = "00000000-0000-0000-0000-000000000000"
-- Configure the preselected profiles for briefing builds.
-- Omit this setting to use the app-wide selection. An empty list means no profiles.
-- A non-empty list replaces the app-wide selection; every entry must be a unique,
-- non-empty profile ID defined in CONFIG["PROFILES"].
-- CONFIG["SETTINGS"]["DataVisualBriefing.PreselectedProfileIds"] = {
-- "00000000-0000-0000-0000-000000000000",
-- }
-- The legacy singular DataVisualBriefing.PreselectedProfile setting remains supported by itself.
-- Supplying the singular and plural setting together is invalid.
--
-- Configure the language the briefing content is written in.
-- Allowed values are: AS_IS, EN_US, EN_GB, ZH_CN, HI_IN, ES_ES, FR_FR, DE_DE, DE_CH, DE_AT,
@ -1066,10 +1080,14 @@ CONFIG["DOCUMENT_ANALYSIS_POLICIES"] = {}
-- -- Tool IDs include: web_search, read_web_page
-- ["AllowedToolIds"] = { "web_search" },
--
-- -- Optional: preselect a provider or profile by ID.
-- -- Optional: preselect a provider and profiles by ID.
-- -- The IDs must exist in CONFIG["LLM_PROVIDERS"] or CONFIG["PROFILES"].
-- -- Omit PreselectedProfileIds to use the app-wide selection, use an empty list
-- -- for no profiles, or provide a unique list to replace the app-wide selection.
-- ["PreselectedProvider"] = "00000000-0000-0000-0000-000000000000",
-- ["PreselectedProfile"] = "00000000-0000-0000-0000-000000000000",
-- ["PreselectedProfileIds"] = { "00000000-0000-0000-0000-000000000000" },
-- -- The legacy singular PreselectedProfile field remains supported by itself.
-- -- Supplying both fields makes the policy invalid.
--
-- -- Optional: hide the policy definition section in the UI.
-- -- When set to true, users will only see the document selection interface

View File

@ -33,6 +33,11 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
/// </summary>
public required TValue Default { get; init; }
/// <summary>
/// Whether a managed user-value snapshot may restore JSON null for this setting.
/// </summary>
public bool AllowNullSnapshot { get; init; }
/// <summary>
/// The additive value contributions, one per contributing configuration plugin.
/// </summary>
@ -70,10 +75,10 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
try
{
var value = JsonSerializer.Deserialize<TValue>(json, SettingsManager.JSON_OPTIONS);
if (value is null)
if (value is null && !this.AllowNullSnapshot)
return false;
this.SetValue(value);
this.SetValue(value!);
return true;
}
catch (Exception e)
@ -116,4 +121,4 @@ public record ConfigMeta<TClass, TValue> : ConfigMetaBase
return default!;
}
}
}

View File

@ -238,17 +238,8 @@ public static class ConfigurationSelectDataFactory
public static IEnumerable<ConfigurationSelectData<string>> GetProfilesData(IEnumerable<Profile> profiles)
{
foreach (var profile in profiles.GetAllProfiles())
yield return new(profile.GetSafeName(), profile.Id);
}
public static IEnumerable<ConfigurationSelectData<ProfilePreselection>> GetComponentProfilesData(IEnumerable<Profile> profiles)
{
yield return new(TB("Use app default profile"), ProfilePreselection.AppDefault);
yield return new(Profile.NO_PROFILE.GetSafeName(), ProfilePreselection.NoProfile);
foreach (var profile in profiles)
yield return new(profile.GetSafeName(), ProfilePreselection.Specific(profile.Id));
yield return new(profile.GetSafeName(), profile.Id);
}
public static IEnumerable<ConfigurationSelectData<string>> GetTranscriptionProvidersData(IEnumerable<TranscriptionProvider> transcriptionProviders)

View File

@ -11,7 +11,7 @@ public sealed class Data
/// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
/// </summary>
public Version Version { get; init; } = Version.V6;
public Version Version { get; init; } = Version.V7;
/// <summary>
/// List of configured providers.

View File

@ -65,5 +65,5 @@ public sealed class DataAgenda
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -98,9 +98,9 @@ public sealed class DataApp(Expression<Func<Data, DataApp>>? configSelection = n
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Should we preselect a profile for the entire app?
/// Which profiles should be preselected for the entire app?
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty);
public HashSet<string> PreselectedProfileIds { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfileIds, []);
/// <summary>
/// Should we preselect a chat template for the entire app?

View File

@ -52,10 +52,10 @@ public sealed class DataBiasOfTheDay
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Preselect a provider?
/// </summary>
public string PreselectedProvider { get; set; } = string.Empty;
}
}

View File

@ -42,9 +42,9 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
public string PreselectedProvider { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProvider, string.Empty);
/// <summary>
/// Preselect a profile?
/// Which profiles should be preselected? Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, n => n.PreselectedProfile, string.Empty);
public HashSet<string>? PreselectedProfileIds { get; set; } = ManagedConfiguration.RegisterProfilePreselection(configSelection, n => n.PreselectedProfileIds);
/// <summary>
/// Preselect a chat template?
@ -97,4 +97,4 @@ public sealed class DataChat(Expression<Func<Data, DataChat>>? configSelection =
/// Should we show the latest message after loading? When false, we show the first (aka oldest) message.
/// </summary>
public bool ShowLatestMessageAfterLoading { get; set; } = true;
}
}

View File

@ -38,5 +38,5 @@ public sealed class DataCoding
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -77,9 +77,9 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
public string PreselectedProvider { get; set; } = string.Empty;
/// <summary>
/// Preselect a profile?
/// Which profiles should be preselected? Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Hide the policy definition section in the UI?
@ -135,9 +135,31 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
if (table.TryGetValue("PreselectedProvider", out var providerValue) && providerValue.TryRead<string>(out var providerId))
preselectedProvider = providerId;
var preselectedProfile = string.Empty;
if (table.TryGetValue("PreselectedProfile", out var profileValue) && profileValue.TryRead<string>(out var profileId))
preselectedProfile = profileId;
var hasLegacyProfile = table.TryGetValue("PreselectedProfile", out var profileValue);
var hasProfileIds = table.TryGetValue("PreselectedProfileIds", out var profileIdsValue);
if (hasLegacyProfile && hasProfileIds)
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains both PreselectedProfile and PreselectedProfileIds.", idx);
return false;
}
HashSet<string>? preselectedProfileIds = null;
if (hasLegacyProfile)
{
if (!profileValue.TryRead<string>(out var profileId) || !TryNormalizeLegacyProfileId(profileId, out preselectedProfileIds))
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains an invalid PreselectedProfile.", idx);
return false;
}
}
else if (hasProfileIds)
{
if (!profileIdsValue.TryRead<LuaTable>(out var profileIdsTable) || !TryReadProfileIds(profileIdsTable, out preselectedProfileIds))
{
LOG.LogWarning("The configured document analysis policy {PolicyIndex} contains invalid PreselectedProfileIds. Expected unique, non-empty GUIDs.", idx);
return false;
}
}
var hidePolicyDefinition = false;
if (table.TryGetValue("HidePolicyDefinition", out var hideValue) && hideValue.TryRead<bool>(out var hide))
@ -171,7 +193,7 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
MinimumProviderConfidence = minimumConfidence,
AllowedToolIds = allowedToolIds,
PreselectedProvider = preselectedProvider,
PreselectedProfile = preselectedProfile,
PreselectedProfileIds = preselectedProfileIds,
HidePolicyDefinition = hidePolicyDefinition,
IsProtected = true,
IsEnterpriseConfiguration = true,
@ -180,4 +202,36 @@ public sealed record DataDocumentAnalysisPolicy : ConfigurationBaseObject
return true;
}
}
private static bool TryNormalizeLegacyProfileId(string profileId, out HashSet<string>? profileIds)
{
profileIds = null;
if (string.IsNullOrWhiteSpace(profileId))
return true;
if (!Guid.TryParse(profileId, out var parsed))
return false;
profileIds = parsed == Guid.Empty ? [] : [parsed.ToString()];
return true;
}
private static bool TryReadProfileIds(LuaTable table, out HashSet<string>? profileIds)
{
var parsedIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = 1; index <= table.ArrayLength; index++)
{
if (!table[index].TryRead<string>(out var profileId) ||
!Guid.TryParse(profileId, out var parsed) ||
parsed == Guid.Empty ||
!parsedIds.Add(parsed.ToString()))
{
profileIds = null;
return false;
}
}
profileIds = parsedIds;
return true;
}
}

View File

@ -48,5 +48,5 @@ public sealed class DataEMail
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -32,5 +32,5 @@ public sealed class DataERI
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -37,5 +37,5 @@ public class DataLegalCheck
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -32,5 +32,5 @@ public sealed class DataMyTasks
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
}
public HashSet<string>? PreselectedProfileIds { get; set; }
}

View File

@ -13,7 +13,7 @@ public class DataSlideBuilder
/// <summary>
/// Preselect a profile?
/// </summary>
public string PreselectedProfile { get; set; } = string.Empty;
public HashSet<string>? PreselectedProfileIds { get; set; }
/// <summary>
/// Preselect a Slide Builder provider?
@ -59,4 +59,4 @@ public class DataSlideBuilder
/// The minimum confidence level required for a provider to be considered.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ConfidenceLevel.NONE;
}
}

View File

@ -19,9 +19,9 @@ public sealed class DataVisualBriefing(Expression<Func<Data, DataVisualBriefing>
}
/// <summary>
/// Gets or sets the preselected profile identifier.
/// Gets or sets the preselected profile identifiers. Null uses the app default.
/// </summary>
public string PreselectedProfile { get; set; } = ManagedConfiguration.Register(configSelection, value => value.PreselectedProfile, string.Empty);
public HashSet<string>? PreselectedProfileIds { get; set; } = ManagedConfiguration.RegisterProfilePreselection(configSelection, value => value.PreselectedProfileIds);
/// <summary>
/// Gets or sets the preselected provider identifier.
@ -72,4 +72,4 @@ public sealed class DataVisualBriefing(Expression<Func<Data, DataVisualBriefing>
/// Gets or sets the minimum confidence accepted for the selected provider.
/// </summary>
public ConfidenceLevel MinimumProviderConfidence { get; set; } = ManagedConfiguration.Register(configSelection, value => value.MinimumProviderConfidence, ConfidenceLevel.NONE);
}
}

View File

@ -0,0 +1,191 @@
using System.Linq.Expressions;
using AIStudio.Settings.DataModel;
using LuaTable = Lua.LuaTable;
using LuaValueType = Lua.LuaValueType;
namespace AIStudio.Settings;
public static partial class ManagedConfiguration
{
public static bool TryProcessProfileIds<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<string>>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
ISet<string> configuredValue = configMeta.Default;
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
successful = TryReadProfileIds(valueTable, out var values);
configuredValue = values;
}
return HandleParsedScalarValue(
configPluginId,
dryRun,
successful,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static bool TryProcessLegacyProfileIds<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, ISet<string>>> propertyExpression,
string legacySettingName,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGet(configSelection, propertyExpression, out var configMeta) ||
!settings.TryGetValue(legacySettingName, out var legacyValue) ||
!legacyValue.TryRead<string>(out var legacyProfileId))
return false;
if (!string.IsNullOrWhiteSpace(legacyProfileId) && !Guid.TryParse(legacyProfileId, out _))
return false;
ISet<string> configuredValue = Guid.TryParse(legacyProfileId, out var profileId) && profileId != Guid.Empty
? new HashSet<string> { profileId.ToString() }
: new HashSet<string>();
return HandleParsedScalarValue(
configPluginId,
dryRun,
true,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static HashSet<string>? RegisterProfilePreselection<TClass>(
Expression<Func<Data, TClass>>? configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression)
{
if (configSelection is null)
return null;
var configPath = Path(configSelection, propertyExpression);
if (!METADATA.ContainsKey(configPath))
{
METADATA[configPath] = new ConfigMeta<TClass, HashSet<string>?>(configSelection, propertyExpression)
{
Default = null,
AllowNullSnapshot = true,
};
}
return null;
}
public static bool TryGetProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
out ConfigMeta<TClass, HashSet<string>?> configMeta)
{
var configPath = Path(configSelection, propertyExpression);
if (METADATA.TryGetValue(configPath, out var value) && value is ConfigMeta<TClass, HashSet<string>?> meta)
{
meta.RestoreLockedConfiguration();
configMeta = meta;
return true;
}
configMeta = new NoConfig<TClass, HashSet<string>?>(configSelection, propertyExpression)
{
Default = null,
};
return false;
}
public static bool TryProcessProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGetProfilePreselection(configSelection, propertyExpression, out var configMeta))
return false;
var successful = false;
HashSet<string>? configuredValue = null;
if (settings.TryGetValue(SettingsManager.ToSettingName(propertyExpression), out var configuredLuaList) &&
configuredLuaList.Type is LuaValueType.Table &&
configuredLuaList.TryRead<LuaTable>(out var valueTable))
{
successful = TryReadProfileIds(valueTable, out var values);
if (successful)
configuredValue = values;
}
if (dryRun)
return successful;
return HandleParsedScalarValue(
configPluginId,
dryRun,
successful,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
public static bool TryProcessLegacyProfilePreselection<TClass>(
Expression<Func<Data, TClass>> configSelection,
Expression<Func<TClass, HashSet<string>?>> propertyExpression,
string legacySettingName,
Guid configPluginId,
LuaTable settings,
bool dryRun)
{
if (!TryGetProfilePreselection(configSelection, propertyExpression, out var configMeta) ||
!settings.TryGetValue(legacySettingName, out var legacyValue) ||
!legacyValue.TryRead<string>(out var legacyProfileId))
return false;
HashSet<string>? configuredValue;
if (string.IsNullOrWhiteSpace(legacyProfileId))
configuredValue = null;
else if (!Guid.TryParse(legacyProfileId, out var parsedProfileId))
return false;
else
configuredValue = parsedProfileId == Guid.Empty ? [] : [parsedProfileId.ToString()];
return HandleParsedScalarValue(
configPluginId,
dryRun,
true,
configMeta,
configuredValue,
ReadManagedConfigurationMode(propertyExpression, settings),
SettingsManager.ToSettingName(propertyExpression));
}
private static bool TryReadProfileIds(LuaTable valueTable, out HashSet<string> values)
{
values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
for (var index = 1; index <= valueTable.ArrayLength; index++)
{
if (!valueTable[index].TryRead<string>(out var value) ||
!Guid.TryParse(value, out var profileId) ||
profileId == Guid.Empty ||
!values.Add(profileId.ToString()))
return false;
}
return true;
}
}

View File

@ -94,6 +94,25 @@ public record Profile(
{actions}
""";
}
public static string ToSystemPrompt(IReadOnlyList<Profile> profiles)
{
if (profiles.Count == 0)
return string.Empty;
var profileSections = profiles.Select((profile, index) =>
$"""
## Profile {index + 1}: {profile.Name}
{profile.ToSystemPrompt()}
""");
return $"""
The user selected the following profiles. Combine them as equally important context. If their instructions conflict, resolve the conflict as well as possible.
{string.Join("\n\n---\n\n", profileSections)}
""";
}
public static bool TryParseProfileTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject template)
{
@ -151,4 +170,4 @@ public record Profile(
}
""";
}
}
}

View File

@ -4,52 +4,54 @@ public readonly record struct ProfilePreselection
{
public ProfilePreselectionMode Mode { get; }
public string SpecificProfileId { get; }
public IReadOnlySet<string> SpecificProfileIds { get; }
public bool UseAppDefault => this.Mode == ProfilePreselectionMode.USE_APP_DEFAULT;
public bool DoNotPreselectProfile => this.Mode == ProfilePreselectionMode.USE_NO_PROFILE;
public bool DoNotPreselectProfiles => this.Mode == ProfilePreselectionMode.USE_NO_PROFILES;
public bool UseSpecificProfile => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILE;
public bool UseSpecificProfiles => this.Mode == ProfilePreselectionMode.USE_SPECIFIC_PROFILES;
public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, string.Empty);
public static ProfilePreselection AppDefault => new(ProfilePreselectionMode.USE_APP_DEFAULT, new HashSet<string>());
public static ProfilePreselection NoProfile => new(ProfilePreselectionMode.USE_NO_PROFILE, Profile.NO_PROFILE.Id);
public static ProfilePreselection NoProfiles => new(ProfilePreselectionMode.USE_NO_PROFILES, new HashSet<string>());
private ProfilePreselection(ProfilePreselectionMode mode, string specificProfileId)
private ProfilePreselection(ProfilePreselectionMode mode, IReadOnlySet<string> specificProfileIds)
{
this.Mode = mode;
this.SpecificProfileId = specificProfileId;
this.SpecificProfileIds = specificProfileIds;
}
public static ProfilePreselection Specific(string profileId)
public static ProfilePreselection Specific(IEnumerable<string> profileIds)
{
if (string.IsNullOrWhiteSpace(profileId))
throw new ArgumentException("A specific profile preselection requires a profile ID.", nameof(profileId));
var normalizedIds = profileIds
.Where(profileId => !string.IsNullOrWhiteSpace(profileId) && !profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Use NoProfile for the NO_PROFILE selection.", nameof(profileId));
if (normalizedIds.Count == 0)
throw new ArgumentException("A specific profile preselection requires at least one profile ID.", nameof(profileIds));
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, profileId);
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILES, normalizedIds);
}
public static ProfilePreselection FromStoredValue(string? storedValue)
public static ProfilePreselection FromStoredValue(IEnumerable<string>? storedValue)
{
if (string.IsNullOrWhiteSpace(storedValue))
if (storedValue is null)
return AppDefault;
if (storedValue.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
return NoProfile;
var profileIds = storedValue
.Where(profileId => !string.IsNullOrWhiteSpace(profileId) && !profileId.Equals(Profile.NO_PROFILE.Id, StringComparison.OrdinalIgnoreCase))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return new(ProfilePreselectionMode.USE_SPECIFIC_PROFILE, storedValue);
return profileIds.Count == 0 ? NoProfiles : Specific(profileIds);
}
public static implicit operator string(ProfilePreselection preselection) => preselection.Mode switch
public static implicit operator HashSet<string>?(ProfilePreselection preselection) => preselection.Mode switch
{
ProfilePreselectionMode.USE_APP_DEFAULT => string.Empty,
ProfilePreselectionMode.USE_NO_PROFILE => Profile.NO_PROFILE.Id,
ProfilePreselectionMode.USE_SPECIFIC_PROFILE => preselection.SpecificProfileId,
_ => string.Empty,
ProfilePreselectionMode.USE_APP_DEFAULT => null,
ProfilePreselectionMode.USE_NO_PROFILES => [],
ProfilePreselectionMode.USE_SPECIFIC_PROFILES => [..preselection.SpecificProfileIds],
_ => null,
};
}
}

View File

@ -3,6 +3,6 @@ namespace AIStudio.Settings;
public enum ProfilePreselectionMode
{
USE_APP_DEFAULT,
USE_NO_PROFILE,
USE_SPECIFIC_PROFILE,
}
USE_NO_PROFILES,
USE_SPECIFIC_PROFILES,
}

View File

@ -19,7 +19,7 @@ public sealed class SettingsManager
public readonly record struct ToolMinimumProviderConfidenceResolution(ConfidenceLevel ConfidenceLevel, string Source);
private const string SETTINGS_FILENAME = "settings.json";
private const Version CURRENT_SETTINGS_VERSION = Version.V6;
private const Version CURRENT_SETTINGS_VERSION = Version.V7;
private readonly record struct SettingsVersionReadResult(Version Version, SettingsWriteBlockReason FailureReason);
@ -28,6 +28,7 @@ public sealed class SettingsManager
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
Converters = { new TolerantEnumConverter() },
};
@ -170,7 +171,10 @@ public sealed class SettingsManager
try
{
await using var settingsStream = File.OpenRead(settingsPath);
using var settingsDocument = await JsonDocument.ParseAsync(settingsStream);
using var settingsDocument = await JsonDocument.ParseAsync(settingsStream, new JsonDocumentOptions
{
AllowTrailingCommas = JSON_OPTIONS.AllowTrailingCommas,
});
if(!settingsDocument.RootElement.TryGetProperty("Version", out var versionElement))
{
this.logger.LogError($"Failed to read the version of the settings file '{settingsPath}'.");
@ -731,29 +735,34 @@ public sealed class SettingsManager
return this.ConfigurationData.TranscriptionProviders.FirstOrDefault(x => x.Id.Equals(transcriptionProviderId, StringComparison.OrdinalIgnoreCase)) ?? TranscriptionProvider.NONE;
}
public Profile GetPreselectedProfile(Tools.Components component)
public IReadOnlyList<Profile> GetPreselectedProfiles(Tools.Components component)
{
var preselection = component.GetProfilePreselection(this);
if (preselection.DoNotPreselectProfile)
return Profile.NO_PROFILE;
if (preselection.DoNotPreselectProfiles)
return [];
if (preselection.UseSpecificProfile)
return this.GetProfileById(preselection.SpecificProfileId);
if (preselection.UseSpecificProfiles)
return this.ResolveProfiles(preselection.SpecificProfileIds);
var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile);
if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile)
return Profile.NO_PROFILE;
return this.GetProfileById(appPreselection.SpecificProfileId);
return this.GetAppPreselectedProfiles();
}
public Profile GetAppPreselectedProfile()
{
var appPreselection = ProfilePreselection.FromStoredValue(this.ConfigurationData.App.PreselectedProfile);
if (appPreselection.DoNotPreselectProfile || !appPreselection.UseSpecificProfile)
return Profile.NO_PROFILE;
public IReadOnlyList<Profile> GetAppPreselectedProfiles() => this.ResolveProfiles(this.ConfigurationData.App.PreselectedProfileIds);
return this.GetProfileById(appPreselection.SpecificProfileId);
public IReadOnlyList<Profile> ResolveProfiles(IEnumerable<string>? profileIds)
{
if (profileIds is null)
return [];
var requestedIds = profileIds
.Where(profileId => !string.IsNullOrWhiteSpace(profileId))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
requestedIds.Remove(Profile.NO_PROFILE.Id);
return this.ConfigurationData.Profiles
.Where(profile => requestedIds.Contains(profile.Id))
.DistinctBy(profile => profile.Id, StringComparer.OrdinalIgnoreCase)
.ToList();
}
public ChatTemplate GetPreselectedChatTemplate(Tools.Components component)

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using AIStudio.Settings.DataModel;
using AIStudio.Settings.DataModel.PreviousModels;
@ -73,16 +74,19 @@ public static class SettingsMigrations
return MigrateV5ToV6(logger, configV5);
case Version.V6:
return MigrateV6ToV7(logger, configData, jsonOptions);
default:
logger.LogInformation("No configuration migration is needed.");
var configV6 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV6 is null)
var configV7 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
if (configV7 is null)
{
logger.LogError("Failed to parse the v6 configuration. Using default values.");
logger.LogError("Failed to parse the v7 configuration. Using default values.");
return new();
}
return configV6;
return configV7;
}
}
@ -266,7 +270,7 @@ public static class SettingsMigrations
logger.LogInformation("Migrating from v5 to v6...");
return new()
{
Version = Version.V6,
Version = Version.V7,
Providers = previousConfig.Providers,
Confidence = new(x => x.Confidence)
{
@ -319,4 +323,104 @@ public static class SettingsMigrations
I18N = previousConfig.I18N,
};
}
}
private static Data MigrateV6ToV7(ILogger<SettingsManager> logger, string configData, JsonSerializerOptions jsonOptions)
{
logger.LogInformation("Migrating from v6 to v7...");
var root = JsonNode.Parse(configData, documentOptions: new JsonDocumentOptions
{
AllowTrailingCommas = jsonOptions.AllowTrailingCommas,
}) as JsonObject;
if (root is null)
{
logger.LogError("Failed to parse the v6 configuration. Using default values.");
return new();
}
root[nameof(Data.Version)] = nameof(Version.V7);
MigrateProfileSetting(root[nameof(Data.App)] as JsonObject, isAppSetting: true);
foreach (var sectionName in new[]
{
nameof(Data.Chat), nameof(Data.Agenda), nameof(Data.Coding), nameof(Data.EMail),
nameof(Data.ERI), nameof(Data.LegalCheck), nameof(Data.MyTasks), nameof(Data.SlideBuilder),
nameof(Data.BiasOfTheDay), nameof(Data.VisualBriefing),
})
MigrateProfileSetting(root[sectionName] as JsonObject, isAppSetting: false);
if (root[nameof(Data.DocumentAnalysis)]?[nameof(DataDocumentAnalysis.Policies)] is JsonArray policies)
{
foreach (var policy in policies.OfType<JsonObject>())
MigrateProfileSetting(policy, isAppSetting: false);
}
MigrateManagedSettingKeys(root[nameof(Data.ManagedLockedConfigurations)] as JsonObject);
MigrateManagedSettingKeys(root[nameof(Data.ManagedEditableDefaults)] as JsonObject);
MigrateManagedSnapshots(root[nameof(Data.ManagedUserValueSnapshots)] as JsonObject);
var migrated = root.Deserialize<Data>(jsonOptions);
if (migrated is not null)
return migrated;
logger.LogError("Failed to deserialize the migrated v7 configuration. Using default values.");
return new();
}
private static void MigrateProfileSetting(JsonObject? section, bool isAppSetting)
{
if (section is null || !section.Remove("PreselectedProfile", out var oldNode))
return;
var oldValue = oldNode?.GetValue<string>() ?? string.Empty;
JsonNode? newValue;
if (string.IsNullOrWhiteSpace(oldValue))
newValue = isAppSetting ? new JsonArray() : null;
else if (Guid.TryParse(oldValue, out var profileId) && profileId == Guid.Empty)
newValue = new JsonArray();
else
newValue = new JsonArray(JsonValue.Create(oldValue));
section["PreselectedProfileIds"] = newValue;
}
private static void MigrateManagedSettingKeys(JsonObject? settings)
{
if (settings is null)
return;
foreach (var oldKey in settings.Select(item => item.Key).Where(key => key.EndsWith(".PreselectedProfile", StringComparison.Ordinal)).ToList())
{
var value = settings[oldKey]?.DeepClone();
settings.Remove(oldKey);
settings[$"{oldKey}Ids"] = value;
}
}
private static void MigrateManagedSnapshots(JsonObject? snapshots)
{
if (snapshots is null)
return;
foreach (var oldKey in snapshots.Select(item => item.Key).Where(key => key.EndsWith(".PreselectedProfile", StringComparison.Ordinal)).ToList())
{
var snapshot = snapshots[oldKey]?.GetValue<string>() ?? "null";
string migratedSnapshot;
try
{
var oldValue = JsonSerializer.Deserialize<string?>(snapshot);
var isAppSetting = oldKey.Equals("DataApp.PreselectedProfile", StringComparison.Ordinal);
migratedSnapshot = string.IsNullOrWhiteSpace(oldValue)
? isAppSetting ? "[]" : "null"
: Guid.TryParse(oldValue, out var profileId) && profileId == Guid.Empty
? "[]"
: JsonSerializer.Serialize(new[] { oldValue });
}
catch (JsonException)
{
migratedSnapshot = oldKey.Equals("DataApp.PreselectedProfile", StringComparison.Ordinal) ? "[]" : "null";
}
snapshots.Remove(oldKey);
snapshots[$"{oldKey}Ids"] = migratedSnapshot;
}
}
}

View File

@ -14,4 +14,5 @@ public enum Version
V4,
V5,
V6,
}
V7,
}

View File

@ -207,23 +207,23 @@ public static class ComponentsExtensions
{
var storedValue = component switch
{
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfile : string.Empty,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfile : string.Empty,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfile : string.Empty,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfile : string.Empty,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfile : string.Empty,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfile : string.Empty,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfile : string.Empty,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfile,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfileIds : null,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.PreselectedProfileIds : null,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.PreselectedProfileIds : null,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.PreselectedProfileIds : null,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.PreselectedProfileIds : null,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProfileIds : null,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.PreselectedProfileIds : null,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.PreselectedProfileIds : null,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.PreselectedProfileIds,
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat:
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfile : string.Empty,
Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.ConfigurationData.Chat.PreselectedProfileIds : null,
// The Document Analysis Assistant does not have a preselected profile at the component level.
// The profile is selected per policy instead. We do this inside the Document Analysis Assistant component:
Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id,
Components.DOCUMENT_ANALYSIS_ASSISTANT => [],
_ => string.Empty,
_ => null,
};
return ProfilePreselection.FromStoredValue(storedValue);
@ -236,4 +236,4 @@ public static class ComponentsExtensions
_ => ChatTemplate.NO_CHAT_TEMPLATE,
};
}
}

View File

@ -1,4 +1,5 @@
namespace AIStudio.Tools.PluginSystem.Assistants;
/// <param name="ProfileIds">The exact profiles to use, an empty list for none, or null for chat defaults.</param>
/// <param name="ToolIds">The tools preselected for the chat, or null when the launcher names none.</param>
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, Guid? ProfileId, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds, IReadOnlyList<string>? ToolIds);
public sealed record AssistantChatLaunchConfiguration(string WorkspaceName, Guid? ProviderId, IReadOnlyList<Guid>? ProfileIds, Guid? ChatTemplateId, IReadOnlyList<Guid>? DataSourceIds, IReadOnlyList<string>? ToolIds);

View File

@ -169,14 +169,20 @@ public static class DirectChatLauncherLuaWriter
builder.AppendLine($" [\"WorkspaceName\"] = \"{Escape(definition.Launch.WorkspaceName.Trim())}\",");
//
// Omitted IDs mean "use the chat defaults", while an empty GUID explicitly selects no
// profile or no chat template. An empty provider GUID has no such meaning and is invalid:
// Omitted IDs mean "use the chat defaults", while an empty profile list or chat-template
// GUID explicitly selects none. An empty provider GUID has no such meaning and is invalid:
//
if (definition.Launch.ProviderId is { } providerId && providerId != Guid.Empty)
builder.AppendLine($" [\"ProviderId\"] = \"{providerId}\",");
if (definition.Launch.ProfileId is { } profileId)
builder.AppendLine($" [\"ProfileId\"] = \"{profileId}\",");
if (definition.Launch.ProfileIds is { } profileIds)
{
builder.AppendLine(" [\"ProfileIds\"] = {");
foreach (var profileId in profileIds)
builder.AppendLine($" \"{profileId}\",");
builder.AppendLine(" },");
}
if (definition.Launch.ChatTemplateId is { } chatTemplateId)
builder.AppendLine($" [\"ChatTemplateId\"] = \"{chatTemplateId}\",");
@ -236,4 +242,4 @@ public static class DirectChatLauncherLuaWriter
.Replace("\r", "\\r", StringComparison.Ordinal)
.Replace("\n", "\\n", StringComparison.Ordinal)
.Replace("\t", "\\t", StringComparison.Ordinal);
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Immutable;
using AIStudio.Settings;
using AIStudio.Tools.PluginSystem.Assistants.DataModel;
using AIStudio.Tools.PluginSystem.Assistants.DataModel.Layout;
using Lua;
@ -241,13 +242,13 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
}
if (!TryReadOptionalGuid(assistantTable, "ProviderId", false, out var providerId, out message) ||
!TryReadOptionalGuid(assistantTable, "ProfileId", true, out var profileId, out message) ||
!TryReadOptionalProfileIds(assistantTable, out var profileIds, out message) ||
!TryReadOptionalGuid(assistantTable, "ChatTemplateId", true, out var chatTemplateId, out message) ||
!TryReadOptionalDataSourceIds(assistantTable, out var dataSourceIds, out message) ||
!TryReadOptionalToolIds(assistantTable, out var toolIds, out message))
return false;
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileId, chatTemplateId, dataSourceIds, toolIds);
this.ChatLaunchConfiguration = new(workspaceName, providerId, profileIds, chatTemplateId, dataSourceIds, toolIds);
return true;
@ -309,6 +310,56 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
return true;
}
private static bool TryReadOptionalProfileIds(LuaTable assistantTable, out IReadOnlyList<Guid>? profileIds, out string message)
{
profileIds = null;
message = string.Empty;
var hasLegacyId = assistantTable.TryGetValue("ProfileId", out _);
var hasIds = assistantTable.TryGetValue("ProfileIds", out var profileIdsValue);
if (hasLegacyId && hasIds)
{
message = TB("The ASSISTANT table contains both ProfileId and ProfileIds. Use only one of them.");
return false;
}
if (hasLegacyId)
{
if (!TryReadOptionalGuid(assistantTable, "ProfileId", true, out var profileId, out message))
return false;
profileIds = profileId == Guid.Empty ? [] : profileId is { } id ? [id] : null;
return true;
}
if (!hasIds)
return true;
if (!profileIdsValue.TryRead<LuaTable>(out var profileIdsTable))
{
message = TB("The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs.");
return false;
}
var parsedIds = new List<Guid>(profileIdsTable.ArrayLength);
var uniqueIds = new HashSet<Guid>();
for (var index = 1; index <= profileIdsTable.ArrayLength; index++)
{
if (!profileIdsTable[index].TryRead<string>(out var idText) ||
!Guid.TryParse(idText, out var parsedId) ||
parsedId == Guid.Empty ||
!uniqueIds.Add(parsedId))
{
message = TB("The ASSISTANT table contains invalid ProfileIds. Expected a list of unique, non-empty GUIDs.");
return false;
}
parsedIds.Add(parsedId);
}
profileIds = parsedIds.ToImmutableArray();
return true;
}
/// <summary>
/// Reads the tools an assistant names: the ones a launcher preselects for its chat, or the ones
/// the assistant itself runs with.
@ -383,12 +434,14 @@ public sealed class PluginAssistants(bool isInternal, LuaState state, PluginType
InitializeState(this.RootComponent.Children, assistantState);
var input = assistantState.ToLuaTable(this.RootComponent?.Children ?? []);
input["profiles"] = new LuaTable();
input["profile"] = new LuaTable
{
["Name"] = string.Empty,
["NeedToKnow"] = string.Empty,
["Actions"] = string.Empty,
["Num"] = 0,
["Id"] = Profile.NO_PROFILE.Id,
["Name"] = Profile.NO_PROFILE.Name,
["NeedToKnow"] = Profile.NO_PROFILE.NeedToKnow,
["Actions"] = Profile.NO_PROFILE.Actions,
["Num"] = Profile.NO_PROFILE.Num,
};
var prompt = await this.TryBuildPromptAsync(input, cancellationToken);

View File

@ -210,6 +210,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
if (!TryValidateMinimumProviderConfidenceConfiguration(settingsTable, out message))
return false;
if (!TryValidateProfilePreselectionConfiguration(settingsTable, out message))
return false;
this.DeclaredSettingsCount = CountDeclaredSettings(settingsTable);
// Config: check for updates, and if so, how often?
@ -346,13 +349,29 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
// Config: preselected provider?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected profile?
ManagedConfiguration.TryProcessConfiguration(x => x.App, x => x.PreselectedProfile, Guid.Empty, this.Id, settingsTable, dryRun);
// Config: preselected profiles? The singular name remains a compatibility alias.
var appProfilesValid = settingsTable.TryGetValue("DataApp.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfileIds(x => x.App, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataApp.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfileIds(x => x.App, x => x.PreselectedProfileIds, "DataApp.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!appProfilesValid)
{
message = TB("The configured app profile preselection is invalid.");
return false;
}
// Config: preselected chat options?
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProvider, Guid.Empty, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedProfile, this.Id, settingsTable, dryRun);
var chatProfilesValid = settingsTable.TryGetValue("DataChat.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfilePreselection(x => x.Chat, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataChat.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfilePreselection(x => x.Chat, x => x.PreselectedProfileIds, "DataChat.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!chatProfilesValid)
{
message = TB("The configured chat profile preselection is invalid.");
return false;
}
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedChatTemplate, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourcesDisabled, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourcesAutomaticSelection, this.Id, settingsTable, dryRun);
@ -360,6 +379,16 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.PreselectedDataSourceIds, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.Chat, x => x.SendToChatDataSourceBehavior, this.Id, settingsTable, dryRun);
var visualBriefingProfilesValid = settingsTable.TryGetValue("DataVisualBriefing.PreselectedProfileIds", out _)
? ManagedConfiguration.TryProcessProfilePreselection(x => x.VisualBriefing, x => x.PreselectedProfileIds, this.Id, settingsTable, dryRun)
: !settingsTable.TryGetValue("DataVisualBriefing.PreselectedProfile", out _) ||
ManagedConfiguration.TryProcessLegacyProfilePreselection(x => x.VisualBriefing, x => x.PreselectedProfileIds, "DataVisualBriefing.PreselectedProfile", this.Id, settingsTable, dryRun);
if (!visualBriefingProfilesValid)
{
message = TB("The configured visual briefing profile preselection is invalid.");
return false;
}
// Config: Batch Processing Assistant defaults?
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.PreselectOptions, this.Id, settingsTable, dryRun);
ManagedConfiguration.TryProcessConfiguration(x => x.BatchProcessing, x => x.InputDirectory, this.Id, settingsTable, dryRun);
@ -391,6 +420,27 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT
return true;
}
private static bool TryValidateProfilePreselectionConfiguration(LuaTable settings, out string message)
{
foreach (var settingPrefix in new[] { "DataApp", "DataChat", "DataVisualBriefing" })
{
var singularName = $"{settingPrefix}.PreselectedProfile";
var pluralName = $"{settingPrefix}.PreselectedProfileIds";
if (settings.TryGetValue(singularName, out _) && settings.TryGetValue(pluralName, out _))
{
message = string.Format(TB("The SETTINGS table contains both '{0}' and '{1}'. Use only one of them."), singularName, pluralName);
return false;
}
var legacyOverrideName = $"{singularName}.AllowUserOverride";
if (settings.TryGetValue(legacyOverrideName, out var legacyOverride))
settings[$"{pluralName}.AllowUserOverride"] = legacyOverride;
}
message = string.Empty;
return true;
}
private static bool TryValidateMinimumProviderConfidenceConfiguration(LuaTable settingsTable, out string message)
{
const string SETTING_NAME = "DataTools.MinimumProviderConfidenceByToolId";

View File

@ -5,8 +5,8 @@ namespace AIStudio.Tools.Services;
/// </summary>
/// <param name="WorkspaceName">The workspace the chat is created in.</param>
/// <param name="ProviderId">The provider to preselect, or null for the chat default.</param>
/// <param name="ProfileId">The profile to preselect; the empty GUID selects no profile.</param>
/// <param name="ProfileIds">The exact profiles to use, an empty list for none, or null for chat defaults.</param>
/// <param name="ChatTemplateId">The chat template to preselect; the empty GUID selects none.</param>
/// <param name="DataSourceIds">The data sources to preselect, or null for the chat defaults.</param>
/// <param name="ToolIds">The tools to preselect, or null for the chat defaults.</param>
public sealed record AssistantBuilderChatLaunchRequest(string WorkspaceName, string? ProviderId, string? ProfileId, string? ChatTemplateId, IReadOnlyList<string>? DataSourceIds, IReadOnlyList<string>? ToolIds);
public sealed record AssistantBuilderChatLaunchRequest(string WorkspaceName, string? ProviderId, IReadOnlyList<string>? ProfileIds, string? ChatTemplateId, IReadOnlyList<string>? DataSourceIds, IReadOnlyList<string>? ToolIds);

View File

@ -164,7 +164,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
new(
chatLaunch.WorkspaceName.Trim(),
ParseOptionalGuid(chatLaunch.ProviderId),
ParseOptionalGuid(chatLaunch.ProfileId),
chatLaunch.ProfileIds?.Select(Guid.Parse).ToArray(),
ParseOptionalGuid(chatLaunch.ChatTemplateId),
chatLaunch.DataSourceIds?.Select(Guid.Parse).ToArray(),
chatLaunch.ToolIds));
@ -505,9 +505,9 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
: $$"""
- Describe a direct chat launcher, not a form assistant.
- Copy the structured ChatLaunch selections faithfully into the {{TB("Chat Launcher")}}, {{TB("Workspace")}}, {{TB("Chat Configuration")}}, {{TB("Data Sources")}}, and {{TB("Tools")}} sections.
- Explain omitted provider, profile, template, data-source, or tool values as using the normal chat defaults.
- Explain omitted provider, profiles, template, data-source, or tool values as using the normal chat defaults.
- In the {{TB("Tools")}} section, say what the preselected tools let the chat do and that users may change the selection once the chat is open.
- Explain the empty profile/template GUID as explicitly selecting no profile/template.
- Explain an empty profile list as explicitly selecting no profiles and the empty template GUID as selecting no template.
- Do not propose UI components, submit behavior, BuildPrompt, or a plugin SystemPrompt for a chat launcher.
""";
@ -619,9 +619,9 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
- Set assistant.kind to "CHAT_LAUNCHER" exactly when the revised ASSISTANT table uses LaunchBehavior = "OPEN_WORKSPACE_CHAT_BY_NAME"; otherwise set it to "FORM".
- For a form assistant, include system_prompt, submit_text, and allow_ai_studio_profiles in the JSON assistant object and omit launch. Include tool_ids exactly when the revised ASSISTANT table carries ToolIds.
- Change ASSISTANT.ToolIds only when the requested change asks for it. Use only tool IDs from the "Available tools" list in the plugin context for tools you add; never invent an ID. Drop the field entirely rather than writing an empty list.
- For a chat launcher, include launch with the exact WorkspaceName and optional ProviderId, ProfileId, ChatTemplateId, DataSourceIds, and ToolIds values from the revised ASSISTANT table; omit system_prompt, submit_text, and allow_ai_studio_profiles.
- For a chat launcher, include launch with the exact WorkspaceName and optional ProviderId, ProfileIds, ChatTemplateId, DataSourceIds, and ToolIds values from the revised ASSISTANT table; omit system_prompt, submit_text, and allow_ai_studio_profiles.
- A chat launcher must not include SystemPrompt, SubmitText, AllowProfiles, BuildPrompt, or UI in its ASSISTANT table.
- Preserve an empty profile or template GUID when it explicitly means no profile or no template. Do not emit empty provider or data-source GUIDs.
- Preserve an empty ProfileIds list when it explicitly means no profiles, and preserve an empty template GUID when it means no template. Do not emit empty provider, profile, or data-source GUIDs.
- Preserve existing behavior unless the requested change explicitly modifies it.
- Apply the requested change directly to plugin.lua; do not describe how to change it.
- Do not create companion files, new require(...) dependencies, hidden behavior, or obfuscated behavior.
@ -754,7 +754,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
if (string.IsNullOrWhiteSpace(launch.WorkspaceName) ||
!IsOptionalGuid(launch.ProviderId, allowEmpty: false) ||
!IsOptionalGuid(launch.ProfileId, allowEmpty: true) ||
!IsOptionalGuidList(launch.ProfileIds, allowEmptyList: true) ||
!IsOptionalGuid(launch.ChatTemplateId, allowEmpty: true))
return false;
@ -784,10 +784,14 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
if (actual is null ||
!string.Equals(requested.WorkspaceName.Trim(), actual.WorkspaceName, StringComparison.Ordinal) ||
ParseOptionalGuid(requested.ProviderId) != actual.ProviderId ||
ParseOptionalGuid(requested.ProfileId) != actual.ProfileId ||
ParseOptionalGuid(requested.ChatTemplateId) != actual.ChatTemplateId)
return false;
var requestedProfileIds = requested.ProfileIds?.Select(Guid.Parse).ToArray();
if (!(requestedProfileIds is null && actual.ProfileIds is null ||
requestedProfileIds is not null && actual.ProfileIds is not null && requestedProfileIds.SequenceEqual(actual.ProfileIds)))
return false;
var requestedDataSourceIds = requested.DataSourceIds?.Select(Guid.Parse).ToArray();
if (!(requestedDataSourceIds is null && actual.DataSourceIds is null ||
requestedDataSourceIds is not null && actual.DataSourceIds is not null &&
@ -832,7 +836,7 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
var request = new AssistantBuilderChatLaunchRequest(
launch.WorkspaceName,
launch.ProviderId,
launch.ProfileId,
launch.ProfileIds,
launch.ChatTemplateId,
launch.DataSourceIds,
launch.ToolIds);
@ -868,6 +872,11 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
private static bool IsOptionalGuid(string? value, bool allowEmpty) => value is null ||
Guid.TryParse(value, out var parsed) && (allowEmpty || parsed != Guid.Empty);
private static bool IsOptionalGuidList(IReadOnlyList<string>? values, bool allowEmptyList) => values is null ||
(allowEmptyList || values.Count > 0) &&
values.All(value => Guid.TryParse(value, out var parsed) && parsed != Guid.Empty) &&
values.Distinct(StringComparer.OrdinalIgnoreCase).Count() == values.Count;
private static Guid? ParseOptionalGuid(string? value) => value is null ? null : Guid.Parse(value);
private static string SerializeUntrustedPromptData(object value) => JsonSerializer.Serialize(value, UNTRUSTED_PROMPT_JSON_OPTIONS);
@ -888,4 +897,4 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
private static AssistantPluginRevisionDraft RevisionFailure(string issue) => new(false, string.Empty, string.Empty, issue);
private readonly record struct AssistantContextFile(string Title, string RelativePath, bool IsRequired);
}
}

View File

@ -21,9 +21,9 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
if (providerResult.IsExplicit && providerResult.Provider == ProviderSettings.NONE)
return new(null, providerResult.ErrorMessage);
var profileResult = this.ResolveProfile(launchConfiguration.ProfileId);
var profile = profileResult.Profile;
if (profile is null)
var profileResult = this.ResolveProfiles(launchConfiguration.ProfileIds);
var profiles = profileResult.Profiles;
if (profiles is null)
return new(null, profileResult.ErrorMessage);
var chatTemplateResult = this.ResolveChatTemplate(launchConfiguration.ChatTemplateId);
@ -36,13 +36,13 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
// its profile selection for such templates, so keeping the profile would pin one that the
// user can neither see nor change. We drop it instead of failing the whole launch.
//
if (!chatTemplate.AllowProfileUsage && profile != Profile.NO_PROFILE)
if (!chatTemplate.AllowProfileUsage && profiles.Count > 0)
{
logger.LogWarning(
"Assistant plugin '{PluginName}' selects the profile '{ProfileName}', but its chat template '{ChatTemplateName}' does not allow profiles. The chat starts without a profile.",
assistantPlugin.Name, profile.GetSafeName(), chatTemplate.GetSafeName());
"Assistant plugin '{PluginName}' selects profiles, but its chat template '{ChatTemplateName}' does not allow profiles. The chat starts without profiles.",
assistantPlugin.Name, chatTemplate.GetSafeName());
profile = Profile.NO_PROFILE;
profiles = [];
}
var dataSourceOptionsResult = await this.ResolveDataSourceOptionsAsync(providerResult.Provider, launchConfiguration.DataSourceIds);
@ -81,7 +81,7 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
{
IncludeDateTime = true,
SelectedProvider = providerResult.Provider == ProviderSettings.NONE ? string.Empty : providerResult.Provider.Id,
SelectedProfile = profile.Id,
SelectedProfileIds = profiles.Select(profile => profile.Id).ToHashSet(StringComparer.OrdinalIgnoreCase),
SelectedChatTemplate = chatTemplate.Id,
// The provider confidence is checked later, when the chat sends a message:
SelectedToolIds = selectedToolIds,
@ -121,23 +121,19 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
return new(provider, true, string.Empty);
}
private (Profile? Profile, string ErrorMessage) ResolveProfile(Guid? profileId)
private (IReadOnlyList<Profile>? Profiles, string ErrorMessage) ResolveProfiles(IReadOnlyList<Guid>? profileIds)
{
if (profileId is null)
return new(settingsManager.GetPreselectedProfile(Components.CHAT), string.Empty);
if (profileIds is null)
return new(settingsManager.GetPreselectedProfiles(Components.CHAT), string.Empty);
// The launcher explicitly wants no profile:
if (profileId == Guid.Empty)
return new(Profile.NO_PROFILE, string.Empty);
if (profileIds.Count == 0)
return new([], string.Empty);
//
// We already handled the empty GUID above, so GetProfileById returning the no-profile
// entry here can only mean that the referenced profile is gone:
//
var profile = settingsManager.GetProfileById(profileId.Value.ToString());
return profile == Profile.NO_PROFILE
? new(null, string.Format(TB("The assistant chat launcher references profile '{0}', but that profile does not exist."), profileId))
: new(profile, string.Empty);
var profiles = settingsManager.ResolveProfiles(profileIds.Select(profileId => profileId.ToString()));
if (profiles.Count != profileIds.Count)
return new(null, TB("The assistant chat launcher references one or more profiles that do not exist."));
return new(profiles, string.Empty);
}
private (ChatTemplate? ChatTemplate, string ErrorMessage) ResolveChatTemplate(Guid? chatTemplateId)
@ -212,4 +208,4 @@ public sealed class DirectChatService(SettingsManager settingsManager, DataSourc
PreselectedDataSourceIds = requestedDataSources.Select(source => source.Id).ToList(),
}, string.Empty);
}
}
}

View File

@ -1,4 +1,5 @@
# v26.9.1, build 256 (2026-09-xx xx:xx UTC)
- Added the option to select several profiles at once throughout AI Studio. Chats, assistants, document-analysis policies, Visual Briefings, saved sessions, and assistant plugins now keep the complete selection, while each area can still use the app default or no profiles at all.
- Added tools that AI models can use on their own, starting with Web Search and Read Web Page. When you ask something a model cannot answer from what it knows, it now searches the web, reads the pages it found, and answers with the sources it used. You decide which tools a model may use, right below the message field, and you can watch it work: AI Studio shows which tool is running and, afterward, every call it made with its result. Whether tools are offered at all depends on the model because it has to support them. Read Web Page works right away; for Web Search you pick a search service in the app settings — Tavily or Staan with a free API key, or a SearXNG instance you run yourself. Set up more than one, and they can take turns when one of them finds nothing, or be asked all at once with their results combined. Many thanks to Peer Schütt (`peerschuett`) and Nils Kruthoff (`nilskruthoff`) for building this feature.
- Added safeguards around everything these tools bring back. Anything fetched from the web is treated as untrusted: AI Studio removes instructions hidden in a page before a model reads it and tells you when it did, exactly as it already does for the documents and web pages you load yourself. A model can never point a tool at your own network. Each tool states how much you have to trust a provider before it may be used with it, so your questions do not travel further than you allow. You can adjust that requirement per tool in the app settings.
- Added tools to the assistants. Each assistant has its own tool settings: which tools it starts with and whether you get to change them while you work. The chat, the coding assistant, and the Slide Builder always show the selection; for every other assistant you switch it on where you want it.