Moved all extension methods into extension blocks (#1155)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2026-11-04 10:46:18 +01:00
1 parent acc7d09fa2
commit c0a75f39ac
138 files changed
+5881 -4951

No files matched your search

+1
View File
@@ -28,6 +28,7 @@ Key structure:
## Important Development Notes ## Important Development Notes
- **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`. - **Naming conventions** - Constants, enum members, and `static readonly` fields use `UPPER_SNAKE_CASE` such as `MY_CONSTANT`.
- **Extension members** - Declare extension methods in an `extension` block of their static class, one block per receiver, never with a `this` parameter: MWAIS0016 refuses the classic form. The `<param>` of the receiver, and of a generic block its `<typeparam>`, belongs on the block, not on its members. The one exception is a method with a `params` parameter, which stays classic until the SDK in `metadata.txt` carries the fix for dotnet/roslyn#80024: for a member of an extension block, the compiler warns about every argument passed one by one to a `params` parameter (CS8620), see `JsRuntimeExtensions`.
- **Message Bus** - Singleton event bus for cross-component communication inside the .NET app - **Message Bus** - Singleton event bus for cross-component communication inside the .NET app
- **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends. - **Prompt injection reports** - The user hears once per action about what the filter removed, as soon as that action ends. Content which belongs to an action carries its scope from `PromptInjectionGuardService.BeginAction` down to where it is read or filtered; whatever is filtered without a scope is reported right away. Pass the scope as a parameter, never through a field or an `AsyncLocal`: the service serves the whole app, and the message bus delivers to other receivers in the execution context of whoever sends.
- **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`. - **Paths in stored data** - A stored path to a file which AI Studio keeps below the data directory, such as a transcript or a tokenizer copy, is relative to that directory, so that a backup or a copy of the data directory works on another computer, under another user name, or on another OS. Mark such a property with `[JsonConverter(typeof(DataDirectoryRelativePathConverter))]`, for a positional record parameter with `[property: ...]`; in memory, the path stays absolute. Better still, derive the path from IDs and store none, as `VisualBriefingStore` does. Paths to files of the user stay absolute. Chats stored before v26.10.1 are migrated by a compatibility shim, see `documentation/compatibility-shims/2026-10-relative-data-paths.md`.
@@ -6,37 +6,40 @@ public static class AssistantAuditLevelExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantAuditLevelExtensions).Namespace, nameof(AssistantAuditLevelExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantAuditLevelExtensions).Namespace, nameof(AssistantAuditLevelExtensions));
public static string GetName(this AssistantAuditLevel level) => level switch extension(AssistantAuditLevel level)
{ {
AssistantAuditLevel.DANGEROUS => TB("Dangerous"), public string GetName() => level switch
AssistantAuditLevel.CAUTION => TB("Concerning"), {
AssistantAuditLevel.SAFE => TB("Safe"), AssistantAuditLevel.DANGEROUS => TB("Dangerous"),
_ => TB("Unknown"), AssistantAuditLevel.CAUTION => TB("Concerning"),
}; AssistantAuditLevel.SAFE => TB("Safe"),
_ => TB("Unknown"),
};
public static Severity GetSeverity(this AssistantAuditLevel level) => level switch public Severity GetSeverity() => level switch
{ {
AssistantAuditLevel.DANGEROUS => Severity.Error, AssistantAuditLevel.DANGEROUS => Severity.Error,
AssistantAuditLevel.CAUTION => Severity.Warning, AssistantAuditLevel.CAUTION => Severity.Warning,
AssistantAuditLevel.SAFE => Severity.Success, AssistantAuditLevel.SAFE => Severity.Success,
_ => Severity.Info, _ => Severity.Info,
}; };
public static Color GetColor(this AssistantAuditLevel level) => level switch public Color GetColor() => level switch
{ {
AssistantAuditLevel.DANGEROUS => Color.Error, AssistantAuditLevel.DANGEROUS => Color.Error,
AssistantAuditLevel.CAUTION => Color.Warning, AssistantAuditLevel.CAUTION => Color.Warning,
AssistantAuditLevel.SAFE => Color.Success, AssistantAuditLevel.SAFE => Color.Success,
_ => Color.Default, _ => Color.Default,
}; };
public static string GetIcon(this AssistantAuditLevel level) => level switch public string GetIcon() => level switch
{ {
AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous, AssistantAuditLevel.DANGEROUS => Icons.Material.Filled.Dangerous,
AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning, AssistantAuditLevel.CAUTION => Icons.Material.Filled.Warning,
AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified, AssistantAuditLevel.SAFE => Icons.Material.Filled.Verified,
_ => Icons.Material.Filled.HelpOutline, _ => Icons.Material.Filled.HelpOutline,
}; };
}
/// <summary> /// <summary>
/// Parses an audit level string and falls back to <see cref="AssistantAuditLevel.UNKNOWN"/> when parsing fails. /// Parses an audit level string and falls back to <see cref="AssistantAuditLevel.UNKNOWN"/> when parsing fails.
@@ -4,23 +4,26 @@ public static class NumberParticipantsExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(NumberParticipantsExtensions).Namespace, nameof(NumberParticipantsExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(NumberParticipantsExtensions).Namespace, nameof(NumberParticipantsExtensions));
public static string Name(this NumberParticipants numberParticipants) => numberParticipants switch extension(NumberParticipants numberParticipants)
{ {
NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"), public string Name() => numberParticipants switch
{
NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"),
NumberParticipants.PEER_TO_PEER => TB("2 (peer to peer)"), NumberParticipants.PEER_TO_PEER => TB("2 (peer to peer)"),
NumberParticipants.SMALL_GROUP => TB("3 - 5 (small group)"), NumberParticipants.SMALL_GROUP => TB("3 - 5 (small group)"),
NumberParticipants.LARGE_GROUP => TB("6 - 12 (large group)"), NumberParticipants.LARGE_GROUP => TB("6 - 12 (large group)"),
NumberParticipants.MULTIPLE_SMALL_GROUPS => TB("13 - 20 (multiple small groups)"), NumberParticipants.MULTIPLE_SMALL_GROUPS => TB("13 - 20 (multiple small groups)"),
NumberParticipants.MULTIPLE_LARGE_GROUPS => TB("21 - 30 (multiple large groups)"), NumberParticipants.MULTIPLE_LARGE_GROUPS => TB("21 - 30 (multiple large groups)"),
NumberParticipants.SYMPOSIUM => TB("31 - 100 (symposium)"), NumberParticipants.SYMPOSIUM => TB("31 - 100 (symposium)"),
NumberParticipants.CONFERENCE => TB("101 - 200 (conference)"), NumberParticipants.CONFERENCE => TB("101 - 200 (conference)"),
NumberParticipants.CONGRESS => TB("201 - 1,000 (congress)"), NumberParticipants.CONGRESS => TB("201 - 1,000 (congress)"),
NumberParticipants.LARGE_EVENT => TB("1,000+ (large event)"), NumberParticipants.LARGE_EVENT => TB("1,000+ (large event)"),
_ => "Unknown" _ => "Unknown"
}; };
}
} }
@@ -6,27 +6,30 @@ public static class BatchProcessingCsvSeparatorExtensions
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingCsvSeparatorExtensions).Namespace, nameof(BatchProcessingCsvSeparatorExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingCsvSeparatorExtensions).Namespace, nameof(BatchProcessingCsvSeparatorExtensions));
public static string Name(this BatchProcessingCsvSeparator separator) => separator switch extension(BatchProcessingCsvSeparator separator)
{ {
BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"), public string Name() => separator switch
BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"), {
BatchProcessingCsvSeparator.PIPE => TB("Vertical bar (|)"), BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"),
BatchProcessingCsvSeparator.TAB => TB("Tab"), BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"),
BatchProcessingCsvSeparator.CUSTOM => TB("Custom character"), BatchProcessingCsvSeparator.PIPE => TB("Vertical bar (|)"),
BatchProcessingCsvSeparator.TAB => TB("Tab"),
BatchProcessingCsvSeparator.CUSTOM => TB("Custom character"),
_ => TB("Unknown"), _ => TB("Unknown"),
}; };
public static char Character(this BatchProcessingCsvSeparator separator, string customSeparator) => separator switch public char Character(string customSeparator) => separator switch
{ {
BatchProcessingCsvSeparator.COMMA => ',', BatchProcessingCsvSeparator.COMMA => ',',
BatchProcessingCsvSeparator.SEMICOLON => ';', BatchProcessingCsvSeparator.SEMICOLON => ';',
BatchProcessingCsvSeparator.PIPE => '|', BatchProcessingCsvSeparator.PIPE => '|',
BatchProcessingCsvSeparator.TAB => '\t', BatchProcessingCsvSeparator.TAB => '\t',
BatchProcessingCsvSeparator.CUSTOM when IsValidCustomSeparator(customSeparator) => customSeparator[0], BatchProcessingCsvSeparator.CUSTOM when IsValidCustomSeparator(customSeparator) => customSeparator[0],
_ => DEFAULT_SEPARATOR, _ => DEFAULT_SEPARATOR,
}; };
}
internal static bool IsValidCustomSeparator(string separator) internal static bool IsValidCustomSeparator(string separator)
{ {
@@ -4,11 +4,14 @@ public static class BatchProcessingOutputModeExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingOutputModeExtensions).Namespace, nameof(BatchProcessingOutputModeExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingOutputModeExtensions).Namespace, nameof(BatchProcessingOutputModeExtensions));
public static string Name(this BatchProcessingOutputMode outputMode) => outputMode switch extension(BatchProcessingOutputMode outputMode)
{ {
BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"), public string Name() => outputMode switch
BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"), {
BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"),
BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"),
_ => TB("Unknown output mode"), _ => TB("Unknown output mode"),
}; };
}
} }
@@ -4,12 +4,15 @@ public static class BatchProcessingPromptSourceExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingPromptSourceExtensions).Namespace, nameof(BatchProcessingPromptSourceExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(BatchProcessingPromptSourceExtensions).Namespace, nameof(BatchProcessingPromptSourceExtensions));
public static string Name(this BatchProcessingPromptSource promptSource) => promptSource switch extension(BatchProcessingPromptSource promptSource)
{ {
BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"), public string Name() => promptSource switch
BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"), {
BatchProcessingPromptSource.FILE_IMPORT => TB("Import from a file (.md)"), BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"),
BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"),
BatchProcessingPromptSource.FILE_IMPORT => TB("Import from a file (.md)"),
_ => TB("Unknown prompt source"), _ => TB("Unknown prompt source"),
}; };
}
} }
@@ -2,22 +2,25 @@ namespace AIStudio.Assistants.Builder;
public static class LuaResponseParseErrorExtension public static class LuaResponseParseErrorExtension
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseError).Namespace, nameof(LuaResponseParseError)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(LuaResponseParseErrorExtension).Namespace, nameof(LuaResponseParseErrorExtension));
public static string GetMessage(this LuaResponseParseError parseError, string technicalDetails) => parseError switch extension(LuaResponseParseError parseError)
{ {
LuaResponseParseError.MISSING_JSON_OBJECT => TB("The model response is missing or unreadable."), public string GetMessage(string technicalDetails) => parseError switch
LuaResponseParseError.INVALID_JSON => string.IsNullOrWhiteSpace(technicalDetails) {
? TB("The model returned an invalid response.") LuaResponseParseError.MISSING_JSON_OBJECT => TB("The model response is missing or unreadable."),
: string.Format(TB("The model returned an invalid response: {0}"), technicalDetails), LuaResponseParseError.INVALID_JSON => string.IsNullOrWhiteSpace(technicalDetails)
LuaResponseParseError.EMPTY_JSON_OBJECT => TB("The model returned an empty JSON object."), ? TB("The model returned an invalid response.")
LuaResponseParseError.UNSUPPORTED_SCHEMA_VERSION => TB("The model responded with an unsupported or deprecated JSON schema."), : string.Format(TB("The model returned an invalid response: {0}"), technicalDetails),
LuaResponseParseError.MISSING_PLUGIN_METADATA => TB("The model's answer is missing the plugin metadata."), LuaResponseParseError.EMPTY_JSON_OBJECT => TB("The model returned an empty JSON object."),
LuaResponseParseError.MISSING_ASSISTANT_METADATA => TB("The model's answer is missing the assistant metadata."), LuaResponseParseError.UNSUPPORTED_SCHEMA_VERSION => TB("The model responded with an unsupported or deprecated JSON schema."),
LuaResponseParseError.INCOMPLETE_PLUGIN_METADATA => TB("The model's answer contains incomplete plugin metadata."), LuaResponseParseError.MISSING_PLUGIN_METADATA => TB("The model's answer is missing the plugin metadata."),
LuaResponseParseError.INCOMPLETE_ASSISTANT_METADATA => TB("The model's answer contains incomplete assistant metadata."), LuaResponseParseError.MISSING_ASSISTANT_METADATA => TB("The model's answer is missing the assistant metadata."),
LuaResponseParseError.MISSING_LUA => TB("The model response does not contain the generated Lua plugin code."), LuaResponseParseError.INCOMPLETE_PLUGIN_METADATA => TB("The model's answer contains incomplete plugin metadata."),
LuaResponseParseError.LUA_MISSING_ID => TB("The generated Lua plugin code does not contain a readable plugin ID."), LuaResponseParseError.INCOMPLETE_ASSISTANT_METADATA => TB("The model's answer contains incomplete assistant metadata."),
_ => TB("The model returned an unusable JSON response."), LuaResponseParseError.MISSING_LUA => TB("The model response does not contain the generated Lua plugin code."),
}; LuaResponseParseError.LUA_MISSING_ID => TB("The generated Lua plugin code does not contain a readable plugin ID."),
_ => TB("The model returned an unusable JSON response."),
};
}
} }
@@ -4,42 +4,45 @@ public static class CommonCodingLanguageExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(CommonCodingLanguageExtensions).Namespace, nameof(CommonCodingLanguageExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(CommonCodingLanguageExtensions).Namespace, nameof(CommonCodingLanguageExtensions));
public static string Name(this CommonCodingLanguages language) => language switch extension(CommonCodingLanguages language)
{ {
CommonCodingLanguages.NONE => TB("None"), public string Name() => language switch
{
CommonCodingLanguages.NONE => TB("None"),
CommonCodingLanguages.BASH => "Bash", CommonCodingLanguages.BASH => "Bash",
CommonCodingLanguages.BLAZOR => ".NET Blazor", CommonCodingLanguages.BLAZOR => ".NET Blazor",
CommonCodingLanguages.C => "C", CommonCodingLanguages.C => "C",
CommonCodingLanguages.CPP => "C++", CommonCodingLanguages.CPP => "C++",
CommonCodingLanguages.CSHARP => "C#", CommonCodingLanguages.CSHARP => "C#",
CommonCodingLanguages.CSS => "CSS", CommonCodingLanguages.CSS => "CSS",
CommonCodingLanguages.FORTRAN => "Fortran", CommonCodingLanguages.FORTRAN => "Fortran",
CommonCodingLanguages.GDSCRIPT => "GDScript", CommonCodingLanguages.GDSCRIPT => "GDScript",
CommonCodingLanguages.GO => "Go", CommonCodingLanguages.GO => "Go",
CommonCodingLanguages.HTML => "HTML", CommonCodingLanguages.HTML => "HTML",
CommonCodingLanguages.JAVA => "Java", CommonCodingLanguages.JAVA => "Java",
CommonCodingLanguages.JAVASCRIPT => "JavaScript", CommonCodingLanguages.JAVASCRIPT => "JavaScript",
CommonCodingLanguages.JSON => "JSON", CommonCodingLanguages.JSON => "JSON",
CommonCodingLanguages.JULIA => "Julia", CommonCodingLanguages.JULIA => "Julia",
CommonCodingLanguages.KOTLIN => "Kotlin", CommonCodingLanguages.KOTLIN => "Kotlin",
CommonCodingLanguages.LUA => "Lua", CommonCodingLanguages.LUA => "Lua",
CommonCodingLanguages.MARKDOWN => "Markdown", CommonCodingLanguages.MARKDOWN => "Markdown",
CommonCodingLanguages.MATHEMATICA => "Mathematica", CommonCodingLanguages.MATHEMATICA => "Mathematica",
CommonCodingLanguages.MATLAB => "MATLAB", CommonCodingLanguages.MATLAB => "MATLAB",
CommonCodingLanguages.PHP => "PHP", CommonCodingLanguages.PHP => "PHP",
CommonCodingLanguages.POWERSHELL => "PowerShell", CommonCodingLanguages.POWERSHELL => "PowerShell",
CommonCodingLanguages.PROLOG => "Prolog", CommonCodingLanguages.PROLOG => "Prolog",
CommonCodingLanguages.PYTHON => "Python", CommonCodingLanguages.PYTHON => "Python",
CommonCodingLanguages.R => "R", CommonCodingLanguages.R => "R",
CommonCodingLanguages.RUBY => "Ruby", CommonCodingLanguages.RUBY => "Ruby",
CommonCodingLanguages.RUST => "Rust", CommonCodingLanguages.RUST => "Rust",
CommonCodingLanguages.SQL => "SQL", CommonCodingLanguages.SQL => "SQL",
CommonCodingLanguages.SWIFT => "Swift", CommonCodingLanguages.SWIFT => "Swift",
CommonCodingLanguages.TYPESCRIPT => "TypeScript", CommonCodingLanguages.TYPESCRIPT => "TypeScript",
CommonCodingLanguages.XML => "XML", CommonCodingLanguages.XML => "XML",
CommonCodingLanguages.OTHER => TB("Other"), CommonCodingLanguages.OTHER => TB("Other"),
_ => TB("Unknown") _ => TB("Unknown")
}; };
}
} }
@@ -4,23 +4,26 @@ public static class WritingStylesExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions));
public static string Name(this WritingStyles style) => style switch extension(WritingStyles style)
{ {
WritingStyles.ACADEMIC => TB("Academic"), public string Name() => style switch
WritingStyles.PERSONAL => TB("Personal"), {
WritingStyles.BUSINESS_FORMAL => TB("Business formal"), WritingStyles.ACADEMIC => TB("Academic"),
WritingStyles.BUSINESS_INFORMAL => TB("Business informal"), WritingStyles.PERSONAL => TB("Personal"),
WritingStyles.BUSINESS_FORMAL => TB("Business formal"),
WritingStyles.BUSINESS_INFORMAL => TB("Business informal"),
_ => TB("Not specified"), _ => TB("Not specified"),
}; };
public static string Prompt(this WritingStyles style) => style switch public string Prompt() => style switch
{ {
WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.", WritingStyles.ACADEMIC => "Use an academic style for communication in an academic context like between students and professors.",
WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.", WritingStyles.PERSONAL => "Use a personal style for communication between friends and family.",
WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.", WritingStyles.BUSINESS_FORMAL => "Use a formal business style for this e-mail.",
WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.", WritingStyles.BUSINESS_INFORMAL => "Use an informal business style for this e-mail.",
_ => "Use a formal business style for this e-mail.", _ => "Use a formal business style for this e-mail.",
}; };
}
} }
@@ -4,12 +4,15 @@ public static class AllowedLLMProvidersExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AllowedLLMProvidersExtensions).Namespace, nameof(AllowedLLMProvidersExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AllowedLLMProvidersExtensions).Namespace, nameof(AllowedLLMProvidersExtensions));
public static string Description(this AllowedLLMProviders provider) => provider switch extension(AllowedLLMProviders provider)
{ {
AllowedLLMProviders.NONE => TB("Please select what kind of LLM provider are allowed for this data source"), public string Description() => provider switch
AllowedLLMProviders.ANY => TB("Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider"), {
AllowedLLMProviders.SELF_HOSTED => TB("Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider"), AllowedLLMProviders.NONE => TB("Please select what kind of LLM provider are allowed for this data source"),
AllowedLLMProviders.ANY => TB("Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider"),
AllowedLLMProviders.SELF_HOSTED => TB("Self-hosted LLM providers are allowed: users cannot choose any cloud-based provider"),
_ => TB("Unknown option was selected") _ => TB("Unknown option was selected")
}; };
}
} }
@@ -4,25 +4,28 @@ public static class AuthExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AuthExtensions).Namespace, nameof(AuthExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AuthExtensions).Namespace, nameof(AuthExtensions));
public static string Name(this Auth auth) => auth switch extension(Auth auth)
{ {
Auth.NONE => TB("No login necessary: useful for public data sources"), public string Name() => auth switch
{
Auth.NONE => TB("No login necessary: useful for public data sources"),
Auth.KERBEROS => TB("Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users"), Auth.KERBEROS => TB("Login by single-sign-on (SSO) using Kerberos: very complex to implement and to operate, useful for many users"),
Auth.USERNAME_PASSWORD => TB("Login by username and password: simple to implement and to operate, useful for few users; easy to use for users"), Auth.USERNAME_PASSWORD => TB("Login by username and password: simple to implement and to operate, useful for few users; easy to use for users"),
Auth.TOKEN => TB("Login by token: simple to implement and to operate, useful for few users; unusual for many users"), Auth.TOKEN => TB("Login by token: simple to implement and to operate, useful for few users; unusual for many users"),
_ => TB("Unknown login method") _ => TB("Unknown login method")
}; };
public static string ToPrompt(this Auth auth) => auth switch public string ToPrompt() => auth switch
{ {
Auth.NONE => "No login is necessary, the data source is public.", Auth.NONE => "No login is necessary, the data source is public.",
Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.", Auth.KERBEROS => "Login by single-sign-on (SSO) using Kerberos.",
Auth.USERNAME_PASSWORD => "Login by username and password.", Auth.USERNAME_PASSWORD => "Login by username and password.",
Auth.TOKEN => "Login by static token per user.", Auth.TOKEN => "Login by static token per user.",
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -4,18 +4,21 @@ public static class DataSourcesExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(DataSourcesExtensions).Namespace, nameof(DataSourcesExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(DataSourcesExtensions).Namespace, nameof(DataSourcesExtensions));
public static string Name(this DataSources dataSource) => dataSource switch extension(DataSources dataSource)
{ {
DataSources.NONE => TB("No data source selected"), public string Name() => dataSource switch
DataSources.CUSTOM => TB("Custom description"), {
DataSources.NONE => TB("No data source selected"),
DataSources.CUSTOM => TB("Custom description"),
DataSources.FILE_SYSTEM => TB("File system (local or network share)"), DataSources.FILE_SYSTEM => TB("File system (local or network share)"),
DataSources.OBJECT_STORAGE => TB("Object storage, like Amazon S3, MinIO, etc."), DataSources.OBJECT_STORAGE => TB("Object storage, like Amazon S3, MinIO, etc."),
DataSources.KEY_VALUE_STORE => TB("Key-Value store, like Redis, etc."), DataSources.KEY_VALUE_STORE => TB("Key-Value store, like Redis, etc."),
DataSources.DOCUMENT_STORE => TB("Document store, like MongoDB, etc."), DataSources.DOCUMENT_STORE => TB("Document store, like MongoDB, etc."),
DataSources.RELATIONAL_DATABASE => TB("Relational database, like MySQL, PostgreSQL, etc."), DataSources.RELATIONAL_DATABASE => TB("Relational database, like MySQL, PostgreSQL, etc."),
DataSources.GRAPH_DATABASE => TB("Graph database, like Neo4j, ArangoDB, etc."), DataSources.GRAPH_DATABASE => TB("Graph database, like Neo4j, ArangoDB, etc."),
_ => TB("Unknown data source") _ => TB("Unknown data source")
}; };
}
} }
@@ -11,13 +11,16 @@ internal static class ERIServerFileOutcomeExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ERIServerFileOutcomeExtensions).Namespace, nameof(ERIServerFileOutcomeExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ERIServerFileOutcomeExtensions).Namespace, nameof(ERIServerFileOutcomeExtensions));
/// <summary>
/// Gets the localized message which explains why a file was not written.
/// </summary>
/// <param name="outcome">What became of the file.</param> /// <param name="outcome">What became of the file.</param>
/// <param name="file">The path of the file, as the model named it.</param> extension(ERIServerFileOutcome outcome)
/// <returns>The localized message.</returns> {
internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file); /// <summary>
/// Gets the localized message which explains why a file was not written.
/// </summary>
/// <param name="file">The path of the file, as the model named it.</param>
/// <returns>The localized message.</returns>
internal string ToUserMessage(string file) => string.Format(ToUserMessageFormat(outcome), file);
}
private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch private static string ToUserMessageFormat(ERIServerFileOutcome outcome) => outcome switch
{ {
@@ -2,14 +2,17 @@ namespace AIStudio.Assistants.ERI;
public static class ERIVersionExtensions public static class ERIVersionExtensions
{ {
public static Task<string> ReadSpecification(this ERIVersion version, WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL()); extension(ERIVersion version)
public static string SpecificationURL(this ERIVersion version)
{ {
var nameLower = version.ToString().ToLowerInvariant(); public Task<string> ReadSpecification(WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE; public string SpecificationURL()
{
var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json";
return $"specs/eri/{filename}";
}
public bool WasSpecificationSelected() => version != ERIVersion.NONE;
}
} }
@@ -4,23 +4,26 @@ public static class OperatingSystemExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(OperatingSystemExtensions).Namespace, nameof(OperatingSystemExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(OperatingSystemExtensions).Namespace, nameof(OperatingSystemExtensions));
public static string Name(this OperatingSystem os) => os switch extension(OperatingSystem os)
{ {
OperatingSystem.NONE => TB("No operating system specified"), public string Name() => os switch
{
OperatingSystem.NONE => TB("No operating system specified"),
OperatingSystem.WINDOWS => TB("Windows"), OperatingSystem.WINDOWS => TB("Windows"),
OperatingSystem.LINUX => TB("Linux"), OperatingSystem.LINUX => TB("Linux"),
_ => TB("Unknown operating system") _ => TB("Unknown operating system")
}; };
public static string ToPrompt(this OperatingSystem os) => os switch public string ToPrompt() => os switch
{ {
OperatingSystem.NONE => "No operating system specified", OperatingSystem.NONE => "No operating system specified",
OperatingSystem.WINDOWS => "Windows", OperatingSystem.WINDOWS => "Windows",
OperatingSystem.LINUX => "Linux", OperatingSystem.LINUX => "Linux",
_ => "Unknown operating system" _ => "Unknown operating system"
}; };
}
} }
@@ -4,43 +4,46 @@ public static class ProgrammingLanguagesExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ProgrammingLanguagesExtensions).Namespace, nameof(ProgrammingLanguagesExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ProgrammingLanguagesExtensions).Namespace, nameof(ProgrammingLanguagesExtensions));
public static string Name(this ProgrammingLanguages language) => language switch extension(ProgrammingLanguages language)
{ {
ProgrammingLanguages.NONE => TB("No programming language selected"), public string Name() => language switch
{
ProgrammingLanguages.NONE => TB("No programming language selected"),
ProgrammingLanguages.C => "C", ProgrammingLanguages.C => "C",
ProgrammingLanguages.CPP => "C++", ProgrammingLanguages.CPP => "C++",
ProgrammingLanguages.CSHARP => "C#", ProgrammingLanguages.CSHARP => "C#",
ProgrammingLanguages.GO => "Go", ProgrammingLanguages.GO => "Go",
ProgrammingLanguages.JAVA => "Java", ProgrammingLanguages.JAVA => "Java",
ProgrammingLanguages.JAVASCRIPT => "JavaScript", ProgrammingLanguages.JAVASCRIPT => "JavaScript",
ProgrammingLanguages.JULIA => "Julia", ProgrammingLanguages.JULIA => "Julia",
ProgrammingLanguages.MATLAB => "MATLAB", ProgrammingLanguages.MATLAB => "MATLAB",
ProgrammingLanguages.PHP => "PHP", ProgrammingLanguages.PHP => "PHP",
ProgrammingLanguages.PYTHON => "Python", ProgrammingLanguages.PYTHON => "Python",
ProgrammingLanguages.RUST => "Rust", ProgrammingLanguages.RUST => "Rust",
ProgrammingLanguages.OTHER => TB("Other"), ProgrammingLanguages.OTHER => TB("Other"),
_ => TB("Unknown") _ => TB("Unknown")
}; };
public static string ToPrompt(this ProgrammingLanguages language) => language switch public string ToPrompt() => language switch
{ {
ProgrammingLanguages.NONE => "No programming language selected", ProgrammingLanguages.NONE => "No programming language selected",
ProgrammingLanguages.C => "C", ProgrammingLanguages.C => "C",
ProgrammingLanguages.CPP => "C++", ProgrammingLanguages.CPP => "C++",
ProgrammingLanguages.CSHARP => "C#", ProgrammingLanguages.CSHARP => "C#",
ProgrammingLanguages.GO => "Go", ProgrammingLanguages.GO => "Go",
ProgrammingLanguages.JAVA => "Java", ProgrammingLanguages.JAVA => "Java",
ProgrammingLanguages.JAVASCRIPT => "JavaScript", ProgrammingLanguages.JAVASCRIPT => "JavaScript",
ProgrammingLanguages.JULIA => "Julia", ProgrammingLanguages.JULIA => "Julia",
ProgrammingLanguages.MATLAB => "MATLAB", ProgrammingLanguages.MATLAB => "MATLAB",
ProgrammingLanguages.PHP => "PHP", ProgrammingLanguages.PHP => "PHP",
ProgrammingLanguages.PYTHON => "Python", ProgrammingLanguages.PYTHON => "Python",
ProgrammingLanguages.RUST => "Rust", ProgrammingLanguages.RUST => "Rust",
ProgrammingLanguages.OTHER => "Other", ProgrammingLanguages.OTHER => "Other",
_ => "Unknown" _ => "Unknown"
}; };
}
} }
@@ -2,39 +2,42 @@ namespace AIStudio.Assistants.IconFinder;
public static class IconSourceExtensions public static class IconSourceExtensions
{ {
public static string Name(this IconSources iconSource) => iconSource switch extension(IconSources iconSource)
{ {
IconSources.FLAT_ICON => "Flaticon", public string Name() => iconSource switch
IconSources.FONT_AWESOME => "Font Awesome", {
IconSources.MATERIAL_ICONS => "Material Icons", IconSources.FLAT_ICON => "Flaticon",
IconSources.FEATHER_ICONS => "Feather Icons", IconSources.FONT_AWESOME => "Font Awesome",
IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons", IconSources.MATERIAL_ICONS => "Material Icons",
IconSources.ICONS8 => "Icons8", IconSources.FEATHER_ICONS => "Feather Icons",
IconSources.BOOTSTRAP_ICONS => "Bootstrap Icons",
IconSources.ICONS8 => "Icons8",
_ => "Generic", _ => "Generic",
}; };
public static string Prompt(this IconSources iconSource) => iconSource switch public string Prompt() => iconSource switch
{ {
IconSources.FLAT_ICON => "My icon source is Flaticon.", IconSources.FLAT_ICON => "My icon source is Flaticon.",
IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.", IconSources.FONT_AWESOME => "I look for an icon on Font Awesome. Please provide just valid icon names. Valid icon names are using the format `fa-icon-name`.",
IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.", IconSources.MATERIAL_ICONS => "I look for a Material icon. Please provide just valid icon names. Valid icon names are using the format `IconName`.",
IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.", IconSources.FEATHER_ICONS => "My icon source is Feather Icons. Please provide just valid icon names. Valid icon names usiing the format `icon-name`.",
IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.", IconSources.BOOTSTRAP_ICONS => "I look for an icon for Bootstrap. Please provide just valid icon names. Valid icon names are using the format `bi-icon-name`.",
IconSources.ICONS8 => "I look for an icon on Icons8.", IconSources.ICONS8 => "I look for an icon on Icons8.",
_ => string.Empty, _ => string.Empty,
}; };
public static string URL(this IconSources iconSource) => iconSource switch public string URL() => iconSource switch
{ {
IconSources.FLAT_ICON => "https://www.flaticon.com/", IconSources.FLAT_ICON => "https://www.flaticon.com/",
IconSources.FONT_AWESOME => "https://fontawesome.com/", IconSources.FONT_AWESOME => "https://fontawesome.com/",
IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/", IconSources.MATERIAL_ICONS => "https://material.io/resources/icons/",
IconSources.FEATHER_ICONS => "https://feathericons.com/", IconSources.FEATHER_ICONS => "https://feathericons.com/",
IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/", IconSources.BOOTSTRAP_ICONS => "https://icons.getbootstrap.com/",
IconSources.ICONS8 => "https://icons8.com/", IconSources.ICONS8 => "https://icons8.com/",
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -4,19 +4,22 @@ public static class SentenceStructureExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(SentenceStructureExtensions).Namespace, nameof(SentenceStructureExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(SentenceStructureExtensions).Namespace, nameof(SentenceStructureExtensions));
public static string Name(this SentenceStructure sentenceStructure) => sentenceStructure switch extension(SentenceStructure sentenceStructure)
{ {
SentenceStructure.ACTIVE => TB("Active voice"), public string Name() => sentenceStructure switch
SentenceStructure.PASSIVE => TB("Passive voice"), {
SentenceStructure.ACTIVE => TB("Active voice"),
SentenceStructure.PASSIVE => TB("Passive voice"),
_ => TB("Not Specified"), _ => TB("Not Specified"),
}; };
public static string Prompt(this SentenceStructure sentenceStructure) => sentenceStructure switch public string Prompt() => sentenceStructure switch
{ {
SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.", SentenceStructure.ACTIVE => " Use an active voice for the sentence structure.",
SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.", SentenceStructure.PASSIVE => " Use a passive voice for the sentence structure.",
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -4,35 +4,38 @@ public static class WritingStylesExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(WritingStylesExtensions).Namespace, nameof(WritingStylesExtensions));
public static string Name(this WritingStyles style) => style switch extension(WritingStyles style)
{ {
WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"), public string Name() => style switch
WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"), {
WritingStyles.SCIENTIFIC => TB("Scientific (scientific papers, research reports)"), WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"),
WritingStyles.JOURNALISTIC => TB("Journalistic (magazines, newspapers, news)"), WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"),
WritingStyles.LITERARY => TB("Literary (fiction, poetry)"), WritingStyles.SCIENTIFIC => TB("Scientific (scientific papers, research reports)"),
WritingStyles.TECHNICAL => TB("Technical (manuals, documentation)"), WritingStyles.JOURNALISTIC => TB("Journalistic (magazines, newspapers, news)"),
WritingStyles.MARKETING => TB("Marketing (advertisements, sales texts)"), WritingStyles.LITERARY => TB("Literary (fiction, poetry)"),
WritingStyles.ACADEMIC => TB("Academic (essays, seminar papers)"), WritingStyles.TECHNICAL => TB("Technical (manuals, documentation)"),
WritingStyles.LEGAL => TB("Legal (legal texts, contracts)"), WritingStyles.MARKETING => TB("Marketing (advertisements, sales texts)"),
WritingStyles.CHANGELOG => TB("Changelog (release notes, version history)"), WritingStyles.ACADEMIC => TB("Academic (essays, seminar papers)"),
WritingStyles.LEGAL => TB("Legal (legal texts, contracts)"),
WritingStyles.CHANGELOG => TB("Changelog (release notes, version history)"),
_ => TB("Not specified"), _ => TB("Not specified"),
}; };
public static string Prompt(this WritingStyles style) => style switch public string Prompt() => style switch
{ {
WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.", WritingStyles.EVERYDAY => "Use a everyday style like for personal texts, social media, and informal communication.",
WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.", WritingStyles.BUSINESS => "Use a business style like for business emails, reports, and presentations. Most important is clarity and professionalism.",
WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.", WritingStyles.SCIENTIFIC => "Use a scientific style like for scientific papers, research reports, and academic writing. Most important is precision and objectivity.",
WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.", WritingStyles.JOURNALISTIC => "Use a journalistic style like for magazines, newspapers, and news. Most important is readability and engaging content.",
WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.", WritingStyles.LITERARY => "Use a literary style like for fiction, poetry, and creative writing. Most important is creativity and emotional impact.",
WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.", WritingStyles.TECHNICAL => "Use a technical style like for manuals, documentation, and technical writing. Most important is clarity and precision.",
WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.", WritingStyles.MARKETING => "Use a marketing style like for advertisements, sales texts, and promotional content. Most important is persuasiveness and engagement.",
WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.", WritingStyles.ACADEMIC => "Use a academic style like for essays, seminar papers, and academic writing. Most important is clarity and objectivity.",
WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.", WritingStyles.LEGAL => "Use a legal style like for legal texts, contracts, and official documents. Most important is precision and legal correctness. Use formal legal language.",
WritingStyles.CHANGELOG => "Use a changelog style like for release notes, version history, and software updates. Most important is clarity and conciseness. The changelog is structured as a Markdown list. Most list items start with one of the following verbs: Added, Changed, Deprecated, Removed, Fixed, Refactored, Improved, or Upgraded -- these verbs should also translated to the target language. Also, changelogs use past tense.", WritingStyles.CHANGELOG => "Use a changelog style like for release notes, version history, and software updates. Most important is clarity and conciseness. The changelog is structured as a Markdown list. Most list items start with one of the following verbs: Added, Changed, Deprecated, Removed, Fixed, Refactored, Improved, or Upgraded -- these verbs should also translated to the target language. Also, changelogs use past tense.",
_ => "Keep the style of the text as it is.", _ => "Keep the style of the text as it is.",
}; };
}
} }
@@ -2,29 +2,32 @@ namespace AIStudio.Assistants.SWOTAnalysis;
internal static class SwotAnalysisCategoryExtensions internal static class SwotAnalysisCategoryExtensions
{ {
/// <summary> extension(SwotAnalysisCategory category)
/// The name of the category in the JSON the models read and write, e.g., "strengths".
/// </summary>
public static string JsonName(this SwotAnalysisCategory category) => category switch
{ {
SwotAnalysisCategory.STRENGTHS => "strengths", /// <summary>
SwotAnalysisCategory.WEAKNESSES => "weaknesses", /// The name of the category in the JSON the models read and write, e.g., "strengths".
SwotAnalysisCategory.OPPORTUNITIES => "opportunities", /// </summary>
SwotAnalysisCategory.THREATS => "threats", public string JsonName() => category switch
{
SwotAnalysisCategory.STRENGTHS => "strengths",
SwotAnalysisCategory.WEAKNESSES => "weaknesses",
SwotAnalysisCategory.OPPORTUNITIES => "opportunities",
SwotAnalysisCategory.THREATS => "threats",
_ => string.Empty, _ => string.Empty,
}; };
/// <summary> /// <summary>
/// The letter the factor IDs of the category start with, e.g., S1 for the first strength. /// The letter the factor IDs of the category start with, e.g., S1 for the first strength.
/// </summary> /// </summary>
public static string FactorIdPrefix(this SwotAnalysisCategory category) => category switch public string FactorIdPrefix() => category switch
{ {
SwotAnalysisCategory.STRENGTHS => "S", SwotAnalysisCategory.STRENGTHS => "S",
SwotAnalysisCategory.WEAKNESSES => "W", SwotAnalysisCategory.WEAKNESSES => "W",
SwotAnalysisCategory.OPPORTUNITIES => "O", SwotAnalysisCategory.OPPORTUNITIES => "O",
SwotAnalysisCategory.THREATS => "T", SwotAnalysisCategory.THREATS => "T",
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -4,23 +4,26 @@ public static class AudienceAgeGroupExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceAgeGroupExtensions).Namespace, nameof(AudienceAgeGroupExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceAgeGroupExtensions).Namespace, nameof(AudienceAgeGroupExtensions));
public static string Name(this AudienceAgeGroup ageGroup) => ageGroup switch extension(AudienceAgeGroup ageGroup)
{ {
AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"), public string Name() => ageGroup switch
AudienceAgeGroup.CHILDREN => TB("Children"), {
AudienceAgeGroup.TEENAGERS => TB("Teenagers"), AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"),
AudienceAgeGroup.ADULTS => TB("Adults"), AudienceAgeGroup.CHILDREN => TB("Children"),
AudienceAgeGroup.TEENAGERS => TB("Teenagers"),
AudienceAgeGroup.ADULTS => TB("Adults"),
_ => TB("Unspecified age group"), _ => TB("Unspecified age group"),
}; };
public static string Prompt(this AudienceAgeGroup ageGroup) => ageGroup switch public string Prompt() => ageGroup switch
{ {
AudienceAgeGroup.UNSPECIFIED => "Do not tailor the text to a specific age group.", AudienceAgeGroup.UNSPECIFIED => "Do not tailor the text to a specific age group.",
AudienceAgeGroup.CHILDREN => "Use simple, concrete language with short sentences and minimal jargon.", AudienceAgeGroup.CHILDREN => "Use simple, concrete language with short sentences and minimal jargon.",
AudienceAgeGroup.TEENAGERS => "Use clear, approachable language with relatable examples and limited jargon.", AudienceAgeGroup.TEENAGERS => "Use clear, approachable language with relatable examples and limited jargon.",
AudienceAgeGroup.ADULTS => "Use adult-appropriate language with clear structure and direct explanations.", AudienceAgeGroup.ADULTS => "Use adult-appropriate language with clear structure and direct explanations.",
_ => "Do not tailor the text to a specific age group.", _ => "Do not tailor the text to a specific age group.",
}; };
}
} }
@@ -4,25 +4,28 @@ public static class AudienceExpertiseExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceExpertiseExtensions).Namespace, nameof(AudienceExpertiseExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceExpertiseExtensions).Namespace, nameof(AudienceExpertiseExtensions));
public static string Name(this AudienceExpertise expertise) => expertise switch extension(AudienceExpertise expertise)
{ {
AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"), public string Name() => expertise switch
AudienceExpertise.NON_EXPERTS => TB("No expertise"), {
AudienceExpertise.BASIC => TB("Basic expertise"), AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"),
AudienceExpertise.INTERMEDIATE => TB("Intermediate expertise"), AudienceExpertise.NON_EXPERTS => TB("No expertise"),
AudienceExpertise.EXPERTS => TB("Experts"), AudienceExpertise.BASIC => TB("Basic expertise"),
AudienceExpertise.INTERMEDIATE => TB("Intermediate expertise"),
AudienceExpertise.EXPERTS => TB("Experts"),
_ => TB("Unspecified expertise"), _ => TB("Unspecified expertise"),
}; };
public static string Prompt(this AudienceExpertise expertise) => expertise switch public string Prompt() => expertise switch
{ {
AudienceExpertise.UNSPECIFIED => "Do not tailor the text to a specific expertise level.", AudienceExpertise.UNSPECIFIED => "Do not tailor the text to a specific expertise level.",
AudienceExpertise.NON_EXPERTS => "Avoid jargon and explain specialized concepts plainly.", AudienceExpertise.NON_EXPERTS => "Avoid jargon and explain specialized concepts plainly.",
AudienceExpertise.BASIC => "Use simple terminology and briefly explain important technical terms.", AudienceExpertise.BASIC => "Use simple terminology and briefly explain important technical terms.",
AudienceExpertise.INTERMEDIATE => "Assume some familiarity with the topic, but still explain important details clearly.", AudienceExpertise.INTERMEDIATE => "Assume some familiarity with the topic, but still explain important details clearly.",
AudienceExpertise.EXPERTS => "Assume deep familiarity with the topic and use precise domain-specific terminology.", AudienceExpertise.EXPERTS => "Assume deep familiarity with the topic and use precise domain-specific terminology.",
_ => "Do not tailor the text to a specific expertise level.", _ => "Do not tailor the text to a specific expertise level.",
}; };
}
} }
@@ -4,29 +4,32 @@ public static class AudienceOrganizationalLevelExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceOrganizationalLevelExtensions).Namespace, nameof(AudienceOrganizationalLevelExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceOrganizationalLevelExtensions).Namespace, nameof(AudienceOrganizationalLevelExtensions));
public static string Name(this AudienceOrganizationalLevel level) => level switch extension(AudienceOrganizationalLevel level)
{ {
AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"), public string Name() => level switch
AudienceOrganizationalLevel.TRAINEES => TB("Trainees"), {
AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => TB("Individual contributors"), AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"),
AudienceOrganizationalLevel.TEAM_LEADS => TB("Team leads"), AudienceOrganizationalLevel.TRAINEES => TB("Trainees"),
AudienceOrganizationalLevel.MANAGERS => TB("Managers"), AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => TB("Individual contributors"),
AudienceOrganizationalLevel.EXECUTIVES => TB("Executives"), AudienceOrganizationalLevel.TEAM_LEADS => TB("Team leads"),
AudienceOrganizationalLevel.BOARD_MEMBERS => TB("Board members"), AudienceOrganizationalLevel.MANAGERS => TB("Managers"),
AudienceOrganizationalLevel.EXECUTIVES => TB("Executives"),
AudienceOrganizationalLevel.BOARD_MEMBERS => TB("Board members"),
_ => TB("Unspecified organizational level"), _ => TB("Unspecified organizational level"),
}; };
public static string Prompt(this AudienceOrganizationalLevel level) => level switch public string Prompt() => level switch
{ {
AudienceOrganizationalLevel.UNSPECIFIED => "Do not tailor the text to a specific organizational level.", AudienceOrganizationalLevel.UNSPECIFIED => "Do not tailor the text to a specific organizational level.",
AudienceOrganizationalLevel.TRAINEES => "Keep the content supportive and introductory. Explain context and avoid assuming prior organizational knowledge.", AudienceOrganizationalLevel.TRAINEES => "Keep the content supportive and introductory. Explain context and avoid assuming prior organizational knowledge.",
AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => "Focus on execution, clarity, responsibilities, and practical next steps.", AudienceOrganizationalLevel.INDIVIDUAL_CONTRIBUTORS => "Focus on execution, clarity, responsibilities, and practical next steps.",
AudienceOrganizationalLevel.TEAM_LEADS => "Focus on coordination, tradeoffs, risks, and concrete actions for a small team.", AudienceOrganizationalLevel.TEAM_LEADS => "Focus on coordination, tradeoffs, risks, and concrete actions for a small team.",
AudienceOrganizationalLevel.MANAGERS => "Focus on planning, priorities, outcomes, risks, and resource implications.", AudienceOrganizationalLevel.MANAGERS => "Focus on planning, priorities, outcomes, risks, and resource implications.",
AudienceOrganizationalLevel.EXECUTIVES => "Focus on strategy, business impact, risks, and the decisions required.", AudienceOrganizationalLevel.EXECUTIVES => "Focus on strategy, business impact, risks, and the decisions required.",
AudienceOrganizationalLevel.BOARD_MEMBERS => "Provide a concise executive-level summary with governance, strategy, risk, and decision relevance.", AudienceOrganizationalLevel.BOARD_MEMBERS => "Provide a concise executive-level summary with governance, strategy, risk, and decision relevance.",
_ => "Do not tailor the text to a specific organizational level.", _ => "Do not tailor the text to a specific organizational level.",
}; };
}
} }
@@ -4,37 +4,40 @@ public static class AudienceProfileExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceProfileExtensions).Namespace, nameof(AudienceProfileExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(AudienceProfileExtensions).Namespace, nameof(AudienceProfileExtensions));
public static string Name(this AudienceProfile profile) => profile switch extension(AudienceProfile profile)
{ {
AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"), public string Name() => profile switch
AudienceProfile.STUDENTS => TB("Students"), {
AudienceProfile.SCIENTISTS => TB("Scientists"), AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"),
AudienceProfile.LAWYERS => TB("Lawyers"), AudienceProfile.STUDENTS => TB("Students"),
AudienceProfile.INVESTORS => TB("Investors"), AudienceProfile.SCIENTISTS => TB("Scientists"),
AudienceProfile.ENGINEERS => TB("Engineers"), AudienceProfile.LAWYERS => TB("Lawyers"),
AudienceProfile.SOFTWARE_DEVELOPERS => TB("Software developers"), AudienceProfile.INVESTORS => TB("Investors"),
AudienceProfile.JOURNALISTS => TB("Journalists"), AudienceProfile.ENGINEERS => TB("Engineers"),
AudienceProfile.HEALTHCARE_PROFESSIONALS => TB("Healthcare professionals"), AudienceProfile.SOFTWARE_DEVELOPERS => TB("Software developers"),
AudienceProfile.PUBLIC_OFFICIALS => TB("Public officials"), AudienceProfile.JOURNALISTS => TB("Journalists"),
AudienceProfile.BUSINESS_PROFESSIONALS => TB("Business professionals"), AudienceProfile.HEALTHCARE_PROFESSIONALS => TB("Healthcare professionals"),
AudienceProfile.PUBLIC_OFFICIALS => TB("Public officials"),
AudienceProfile.BUSINESS_PROFESSIONALS => TB("Business professionals"),
_ => TB("Unspecified audience profile"), _ => TB("Unspecified audience profile"),
}; };
public static string Prompt(this AudienceProfile profile) => profile switch public string Prompt() => profile switch
{ {
AudienceProfile.UNSPECIFIED => "Do not tailor the text to a specific audience profile.", AudienceProfile.UNSPECIFIED => "Do not tailor the text to a specific audience profile.",
AudienceProfile.STUDENTS => "Write for students. Keep it structured, easy to study, and focused on key takeaways.", AudienceProfile.STUDENTS => "Write for students. Keep it structured, easy to study, and focused on key takeaways.",
AudienceProfile.SCIENTISTS => "Use precise, technical language. Structure the content logically and focus on methods, evidence, and results.", AudienceProfile.SCIENTISTS => "Use precise, technical language. Structure the content logically and focus on methods, evidence, and results.",
AudienceProfile.LAWYERS => "Write with precise wording. Emphasize definitions, implications, compliance, and risks.", AudienceProfile.LAWYERS => "Write with precise wording. Emphasize definitions, implications, compliance, and risks.",
AudienceProfile.INVESTORS => "Focus on market potential, business model, differentiation, traction, risks, and financial upside.", AudienceProfile.INVESTORS => "Focus on market potential, business model, differentiation, traction, risks, and financial upside.",
AudienceProfile.ENGINEERS => "Be technically precise and practical. Focus on systems, constraints, implementation, and tradeoffs.", AudienceProfile.ENGINEERS => "Be technically precise and practical. Focus on systems, constraints, implementation, and tradeoffs.",
AudienceProfile.SOFTWARE_DEVELOPERS => "Use concise technical language. Focus on architecture, implementation details, tradeoffs, and maintainability.", AudienceProfile.SOFTWARE_DEVELOPERS => "Use concise technical language. Focus on architecture, implementation details, tradeoffs, and maintainability.",
AudienceProfile.JOURNALISTS => "Be clear, factual, and concise. Highlight the most newsworthy points and explain relevance plainly.", AudienceProfile.JOURNALISTS => "Be clear, factual, and concise. Highlight the most newsworthy points and explain relevance plainly.",
AudienceProfile.HEALTHCARE_PROFESSIONALS => "Use accurate professional language. Emphasize outcomes, safety, evidence, and practical implications.", AudienceProfile.HEALTHCARE_PROFESSIONALS => "Use accurate professional language. Emphasize outcomes, safety, evidence, and practical implications.",
AudienceProfile.PUBLIC_OFFICIALS => "Focus on public impact, feasibility, budget, compliance, risks, and implementation in a neutral institutional tone.", AudienceProfile.PUBLIC_OFFICIALS => "Focus on public impact, feasibility, budget, compliance, risks, and implementation in a neutral institutional tone.",
AudienceProfile.BUSINESS_PROFESSIONALS => "Be clear, practical, and concise. Focus on business relevance, decisions, and next steps.", AudienceProfile.BUSINESS_PROFESSIONALS => "Be clear, practical, and concise. Focus on business relevance, decisions, and next steps.",
_ => "Do not tailor the text to a specific audience profile.", _ => "Do not tailor the text to a specific audience profile.",
}; };
}
} }
@@ -4,53 +4,56 @@ public static class PresentationOccasionExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(PresentationOccasionExtensions).Namespace, nameof(PresentationOccasionExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(PresentationOccasionExtensions).Namespace, nameof(PresentationOccasionExtensions));
public static string Name(this PresentationOccasion occasion) => occasion switch
{
PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"),
PresentationOccasion.TALK => TB("Talk"),
PresentationOccasion.KEYNOTE => TB("Keynote"),
PresentationOccasion.CONFERENCE_TALK => TB("Conference talk"),
PresentationOccasion.LECTURE => TB("Lecture"),
PresentationOccasion.TRAINING => TB("Training"),
PresentationOccasion.WORKSHOP => TB("Workshop of one day"),
PresentationOccasion.MULTI_DAY_WORKSHOP => TB("Workshop of several days"),
PresentationOccasion.THESIS_DEFENSE => TB("Defense of a thesis or dissertation"),
PresentationOccasion.RESEARCH_RESULTS => TB("Presentation of research results"),
PresentationOccasion.PROJECT_REPORT => TB("Project or status report"),
PresentationOccasion.MANAGEMENT_REPORT => TB("Report to management"),
PresentationOccasion.PITCH => TB("Pitch"),
PresentationOccasion.PRODUCT_PRESENTATION => TB("Product presentation"),
PresentationOccasion.KICK_OFF => TB("Kick-off"),
PresentationOccasion.TEAM_MEETING => TB("Team meeting"),
PresentationOccasion.OTHER => TB("Other occasion"),
_ => TB("Unspecified occasion"),
};
/// <summary>
/// Tells the model what the occasion means for the structure of the presentation.
/// </summary>
/// <param name="occasion">The occasion.</param> /// <param name="occasion">The occasion.</param>
/// <param name="customOccasion">The occasion the user described, for OTHER.</param> extension(PresentationOccasion occasion)
public static string Prompt(this PresentationOccasion occasion, string customOccasion) => occasion switch
{ {
PresentationOccasion.TALK => "The presentation accompanies a talk. Structure it for a speaker who talks the audience through it.", public string Name() => occasion switch
PresentationOccasion.KEYNOTE => "The presentation accompanies a keynote. Favor few words per slide, strong key messages, and a story which carries the audience.", {
PresentationOccasion.CONFERENCE_TALK => "The presentation accompanies a talk at a conference. Keep to the essentials of a short time slot, and end with the key messages and room for questions.", PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"),
PresentationOccasion.LECTURE => "The presentation accompanies a lecture. Teach step by step, with examples, and where it suits, end chapters with an interim summary or with room for questions.", PresentationOccasion.TALK => TB("Talk"),
PresentationOccasion.TRAINING => "The presentation accompanies a training. Teach practical skills step by step, with examples and exercises where the content offers them.", PresentationOccasion.KEYNOTE => TB("Keynote"),
PresentationOccasion.WORKSHOP => "The presentation accompanies a workshop of one day. Plan for interaction, e.g., exercises, group work, or discussions, and for the breaks the schedule of the content has.", PresentationOccasion.CONFERENCE_TALK => TB("Conference talk"),
PresentationOccasion.MULTI_DAY_WORKSHOP => "The presentation accompanies a workshop of several days. Plan for interaction, mark where a day ends and the next one starts, and look back at the start of each day, following the schedule of the content.", PresentationOccasion.LECTURE => TB("Lecture"),
PresentationOccasion.THESIS_DEFENSE => "The presentation defends a thesis or a dissertation before a committee. Present the research questions, the method, the results, and the contribution precisely, and anticipate critical questions.", PresentationOccasion.TRAINING => TB("Training"),
PresentationOccasion.RESEARCH_RESULTS => "The presentation presents research results. Present the questions, the method, the results, and their meaning precisely and comprehensibly.", PresentationOccasion.WORKSHOP => TB("Workshop of one day"),
PresentationOccasion.PROJECT_REPORT => "The presentation reports on a project. Present the status, the progress, the problems, and the next steps clearly.", PresentationOccasion.MULTI_DAY_WORKSHOP => TB("Workshop of several days"),
PresentationOccasion.MANAGEMENT_REPORT => "The presentation reports to management and may prepare a decision. Lead with the key messages, keep details short, and state clearly what is to be decided.", PresentationOccasion.THESIS_DEFENSE => TB("Defense of a thesis or dissertation"),
PresentationOccasion.PITCH => "The presentation pitches an idea, a product, or an offer. Lead with the benefit for the audience, and end with a clear call to action.", PresentationOccasion.RESEARCH_RESULTS => TB("Presentation of research results"),
PresentationOccasion.PRODUCT_PRESENTATION => "The presentation presents a product. Show what it does and what it brings the audience, with examples.", PresentationOccasion.PROJECT_REPORT => TB("Project or status report"),
PresentationOccasion.KICK_OFF => "The presentation opens a project at its kick-off. Present the goals, the team, the plan, and how the work is organized.", PresentationOccasion.MANAGEMENT_REPORT => TB("Report to management"),
PresentationOccasion.TEAM_MEETING => "The presentation serves a team meeting. Keep it short and practical, with clear points and next steps.", PresentationOccasion.PITCH => TB("Pitch"),
PresentationOccasion.OTHER => $"The presentation is for this occasion: {customOccasion}. Structure it accordingly.", PresentationOccasion.PRODUCT_PRESENTATION => TB("Product presentation"),
PresentationOccasion.KICK_OFF => TB("Kick-off"),
PresentationOccasion.TEAM_MEETING => TB("Team meeting"),
PresentationOccasion.OTHER => TB("Other occasion"),
_ => "No particular occasion is given. Structure the presentation by its content.", _ => TB("Unspecified occasion"),
}; };
/// <summary>
/// Tells the model what the occasion means for the structure of the presentation.
/// </summary>
/// <param name="customOccasion">The occasion the user described, for OTHER.</param>
public string Prompt(string customOccasion) => occasion switch
{
PresentationOccasion.TALK => "The presentation accompanies a talk. Structure it for a speaker who talks the audience through it.",
PresentationOccasion.KEYNOTE => "The presentation accompanies a keynote. Favor few words per slide, strong key messages, and a story which carries the audience.",
PresentationOccasion.CONFERENCE_TALK => "The presentation accompanies a talk at a conference. Keep to the essentials of a short time slot, and end with the key messages and room for questions.",
PresentationOccasion.LECTURE => "The presentation accompanies a lecture. Teach step by step, with examples, and where it suits, end chapters with an interim summary or with room for questions.",
PresentationOccasion.TRAINING => "The presentation accompanies a training. Teach practical skills step by step, with examples and exercises where the content offers them.",
PresentationOccasion.WORKSHOP => "The presentation accompanies a workshop of one day. Plan for interaction, e.g., exercises, group work, or discussions, and for the breaks the schedule of the content has.",
PresentationOccasion.MULTI_DAY_WORKSHOP => "The presentation accompanies a workshop of several days. Plan for interaction, mark where a day ends and the next one starts, and look back at the start of each day, following the schedule of the content.",
PresentationOccasion.THESIS_DEFENSE => "The presentation defends a thesis or a dissertation before a committee. Present the research questions, the method, the results, and the contribution precisely, and anticipate critical questions.",
PresentationOccasion.RESEARCH_RESULTS => "The presentation presents research results. Present the questions, the method, the results, and their meaning precisely and comprehensibly.",
PresentationOccasion.PROJECT_REPORT => "The presentation reports on a project. Present the status, the progress, the problems, and the next steps clearly.",
PresentationOccasion.MANAGEMENT_REPORT => "The presentation reports to management and may prepare a decision. Lead with the key messages, keep details short, and state clearly what is to be decided.",
PresentationOccasion.PITCH => "The presentation pitches an idea, a product, or an offer. Lead with the benefit for the audience, and end with a clear call to action.",
PresentationOccasion.PRODUCT_PRESENTATION => "The presentation presents a product. Show what it does and what it brings the audience, with examples.",
PresentationOccasion.KICK_OFF => "The presentation opens a project at its kick-off. Present the goals, the team, the plan, and how the work is organized.",
PresentationOccasion.TEAM_MEETING => "The presentation serves a team meeting. Keep it short and practical, with clear points and next steps.",
PresentationOccasion.OTHER => $"The presentation is for this occasion: {customOccasion}. Structure it accordingly.",
_ => "No particular occasion is given. Structure the presentation by its content.",
};
}
} }
@@ -11,20 +11,23 @@ internal static class TemplateSlideExtensions
/// </summary> /// </summary>
private const int MAX_SLIDE_TITLE_CHARACTERS = 60; private const int MAX_SLIDE_TITLE_CHARACTERS = 60;
/// <summary> extension(TemplateSlide slide)
/// How the forms name a slide of the template: by its number and its title, and what is special about it.
/// </summary>
/// <remarks>
/// The title tells the slides apart at a glance, since a template rarely gives its slides more
/// than a title. A long one is cut, so that the label stays on one line.
/// </remarks>
public static string Label(this TemplateSlide slide)
{ {
var title = slide.Title.Length > MAX_SLIDE_TITLE_CHARACTERS ? $"{slide.Title[..MAX_SLIDE_TITLE_CHARACTERS].TrimEnd()}…" : slide.Title; /// <summary>
var label = title.Length > 0 ? string.Format(TB("Slide {0}: {1}"), slide.Number, title) : string.Format(TB("Slide {0}"), slide.Number); /// How the forms name a slide of the template: by its number and its title, and what is special about it.
if (slide.IsHidden) /// </summary>
label = $"{label} ({TB("hidden")})"; /// <remarks>
/// The title tells the slides apart at a glance, since a template rarely gives its slides more
/// than a title. A long one is cut, so that the label stays on one line.
/// </remarks>
public string Label()
{
var title = slide.Title.Length > MAX_SLIDE_TITLE_CHARACTERS ? $"{slide.Title[..MAX_SLIDE_TITLE_CHARACTERS].TrimEnd()}…" : slide.Title;
var label = title.Length > 0 ? string.Format(TB("Slide {0}: {1}"), slide.Number, title) : string.Format(TB("Slide {0}"), slide.Number);
if (slide.IsHidden)
label = $"{label} ({TB("hidden")})";
return label; return label;
}
} }
} }
@@ -6,12 +6,15 @@ internal static class TemplateSlideUseExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(TemplateSlideUseExtensions).Namespace, nameof(TemplateSlideUseExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(TemplateSlideUseExtensions).Namespace, nameof(TemplateSlideUseExtensions));
public static string Name(this TemplateSlideUse use) => use switch extension(TemplateSlideUse use)
{ {
TemplateSlideUse.OMIT => TB("Leave out"), public string Name() => use switch
TemplateSlideUse.KEEP => TB("Keep unchanged"), {
TemplateSlideUse.EDIT => TB("Let the LLM edit it"), TemplateSlideUse.OMIT => TB("Leave out"),
TemplateSlideUse.KEEP => TB("Keep unchanged"),
TemplateSlideUse.EDIT => TB("Let the LLM edit it"),
_ => TB("Leave out"), _ => TB("Leave out"),
}; };
}
} }
@@ -4,31 +4,34 @@ public static class ComplexityExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ComplexityExtensions).Namespace, nameof(ComplexityExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(ComplexityExtensions).Namespace, nameof(ComplexityExtensions));
public static string Name(this Complexity complexity) => complexity switch extension(Complexity complexity)
{ {
Complexity.NO_CHANGE => TB("No change in complexity"), public string Name() => complexity switch
{
Complexity.NO_CHANGE => TB("No change in complexity"),
Complexity.SIMPLE_LANGUAGE => TB("Simple language, e.g., for children"), Complexity.SIMPLE_LANGUAGE => TB("Simple language, e.g., for children"),
Complexity.TEEN_LANGUAGE => TB("Teen language, e.g., for teenagers"), Complexity.TEEN_LANGUAGE => TB("Teen language, e.g., for teenagers"),
Complexity.EVERYDAY_LANGUAGE => TB("Everyday language, e.g., for adults"), Complexity.EVERYDAY_LANGUAGE => TB("Everyday language, e.g., for adults"),
Complexity.POPULAR_SCIENCE_LANGUAGE => TB("Popular science language, e.g., for people interested in science"), Complexity.POPULAR_SCIENCE_LANGUAGE => TB("Popular science language, e.g., for people interested in science"),
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => TB("Scientific language for experts in this field"), Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => TB("Scientific language for experts in this field"),
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => TB("Scientific language for experts from other fields (interdisciplinary)"), Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => TB("Scientific language for experts from other fields (interdisciplinary)"),
_ => TB("No change in complexity"), _ => TB("No change in complexity"),
}; };
public static string Prompt(this Complexity complexity, string expertInField) => complexity switch public string Prompt(string expertInField) => complexity switch
{ {
Complexity.NO_CHANGE => "Do not change the complexity of the text.", Complexity.NO_CHANGE => "Do not change the complexity of the text.",
Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.", Complexity.SIMPLE_LANGUAGE => "Simplify the language, e.g., for 8 to 12-year-old children. Might use short sentences and simple words. You could use analogies to explain complex terms.",
Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.", Complexity.TEEN_LANGUAGE => "Use a language suitable for teenagers, e.g., 16 to 19 years old. Might use teenage slang and analogies to explain complex terms.",
Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.", Complexity.EVERYDAY_LANGUAGE => "Use everyday language suitable for adults. Avoid specific scientific terms. Use everday analogies to explain complex terms.",
Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.", Complexity.POPULAR_SCIENCE_LANGUAGE => "Use popular science language, e.g., for people interested in science. The text should be easy to understand, though. Use analogies to explain complex terms.",
Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.", Complexity.SCIENTIFIC_LANGUAGE_FIELD_EXPERTS => "Use scientific language for experts in the field of the texts subject. Use specific terms for this field.",
Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.", Complexity.SCIENTIFIC_LANGUAGE_OTHER_EXPERTS => $"The reader is an expert in {expertInField}. Change the language so that it is suitable. Explain specific terms, so that the reader within his field can understand the text. You might use analogies to explain complex terms.",
_ => "Do not change the complexity of the text.", _ => "Do not change the complexity of the text.",
}; };
}
} }
@@ -13,12 +13,15 @@ internal static class VisualBriefingFailureExtensions
{ {
private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(VisualBriefingFailureExtensions).Namespace, nameof(VisualBriefingFailureExtensions)); private static string TB(string fallbackEN) => Tools.PluginSystem.I18N.I.T(fallbackEN, typeof(VisualBriefingFailureExtensions).Namespace, nameof(VisualBriefingFailureExtensions));
/// <summary>
/// Gets the localized message for one recorded failure.
/// </summary>
/// <param name="failure">The recorded failure.</param> /// <param name="failure">The recorded failure.</param>
/// <returns>The localized message.</returns> extension(VisualBriefingFailure failure)
internal static string ToUserMessage(this VisualBriefingFailure failure) => ToUserMessage(failure.Code, failure.ValidationRule); {
/// <summary>
/// Gets the localized message for one recorded failure.
/// </summary>
/// <returns>The localized message.</returns>
internal string ToUserMessage() => ToUserMessage(failure.Code, failure.ValidationRule);
}
/// <summary> /// <summary>
/// Gets the localized message for one failure code and validation rule. /// Gets the localized message for one failure code and validation rule.
@@ -40,70 +43,76 @@ internal static class VisualBriefingFailureExtensions
_ => code.ToUserMessage(), _ => code.ToUserMessage(),
}; };
/// <summary>
/// Gets the localized message for one validation rule.
/// </summary>
/// <param name="rule">The stable validation rule.</param> /// <param name="rule">The stable validation rule.</param>
/// <returns>The localized message.</returns> extension(VisualBriefingValidationRule rule)
private static string ToUserMessage(this VisualBriefingValidationRule rule) => rule switch
{ {
VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), /// <summary>
VisualBriefingValidationRule.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. Please try again or select another model."), /// Gets the localized message for one validation rule.
VisualBriefingValidationRule.UNKNOWN_FIELD => TB("The model response contained unexpected fields. Please try again or select another model."), /// </summary>
VisualBriefingValidationRule.CONTRACT_VERSION_UNSUPPORTED => TB("The model response used an unsupported contract version. Please try again or select another model."), /// <returns>The localized message.</returns>
VisualBriefingValidationRule.ID_INVALID => TB("The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."), private string ToUserMessage() => rule switch
VisualBriefingValidationRule.REFERENCE_INVALID => TB("The model response referenced content that does not exist. Please try again or select another model."), {
VisualBriefingValidationRule.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."),
VisualBriefingValidationRule.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), VisualBriefingValidationRule.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. Please try again or select another model."),
VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID => TB("The model did not fill every planned content slot exactly once. Please try again or select another model."), VisualBriefingValidationRule.UNKNOWN_FIELD => TB("The model response contained unexpected fields. Please try again or select another model."),
VisualBriefingValidationRule.SLOT_VALUE_TYPE_INVALID => TB("The model filled a content slot with the wrong kind of value. Please try again or select another model."), VisualBriefingValidationRule.CONTRACT_VERSION_UNSUPPORTED => TB("The model response used an unsupported contract version. Please try again or select another model."),
VisualBriefingValidationRule.CHART_SET_INVALID => TB("The charts of the model response did not match the planned briefing elements. Please try again or select another model."), VisualBriefingValidationRule.ID_INVALID => TB("The model response contained an empty, malformed, or duplicated identifier. Please try again or select another model."),
VisualBriefingValidationRule.CHART_DATA_INVALID => TB("A chart of the model response contained invalid categories or data series. Please try again or select another model."), VisualBriefingValidationRule.REFERENCE_INVALID => TB("The model response referenced content that does not exist. Please try again or select another model."),
VisualBriefingValidationRule.CONTROL_ID_INVALID => TB("An interactive control of the model response used an invalid identifier. Please try again or select another model."), VisualBriefingValidationRule.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."),
VisualBriefingValidationRule.CONTROL_TARGET_INVALID => TB("An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."), VisualBriefingValidationRule.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."),
VisualBriefingValidationRule.CONTROL_STATE_INVALID => TB("An interactive control of the model response used an invalid initial state. Please try again or select another model."), VisualBriefingValidationRule.SLOT_FULFILLMENT_INVALID => TB("The model did not fill every planned content slot exactly once. Please try again or select another model."),
VisualBriefingValidationRule.CONTROL_REQUIREMENT_INVALID => TB("A briefing element of the model response was missing its required interactive controls. Please try again or select another model."), VisualBriefingValidationRule.SLOT_VALUE_TYPE_INVALID => TB("The model filled a content slot with the wrong kind of value. Please try again or select another model."),
VisualBriefingValidationRule.FORMULA_TARGET_INVALID => TB("A calculation of the model response targeted an invalid briefing element. Please try again or select another model."), VisualBriefingValidationRule.CHART_SET_INVALID => TB("The charts of the model response did not match the planned briefing elements. Please try again or select another model."),
VisualBriefingValidationRule.FORMULA_AST_INVALID => TB("A calculation of the model response used an invalid operation. Please try again or select another model."), VisualBriefingValidationRule.CHART_DATA_INVALID => TB("A chart of the model response contained invalid categories or data series. Please try again or select another model."),
VisualBriefingValidationRule.ACCESSIBILITY_SET_INVALID => TB("The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."), VisualBriefingValidationRule.CONTROL_ID_INVALID => TB("An interactive control of the model response used an invalid identifier. Please try again or select another model."),
VisualBriefingValidationRule.ACCESSIBILITY_TEXT_INVALID => TB("An accessibility text of the model response was empty or invalid. Please try again or select another model."), VisualBriefingValidationRule.CONTROL_TARGET_INVALID => TB("An interactive control of the model response targeted an invalid briefing element. Please try again or select another model."),
VisualBriefingValidationRule.LAYOUT_INVALID => TB("The model response used an invalid briefing layout. Please try again or select another model."), VisualBriefingValidationRule.CONTROL_STATE_INVALID => TB("An interactive control of the model response used an invalid initial state. Please try again or select another model."),
VisualBriefingValidationRule.TEMPLATE_ATTRIBUTE_PROHIBITED => TB("The model response used a prohibited attribute. Please try again or select another model."), VisualBriefingValidationRule.CONTROL_REQUIREMENT_INVALID => TB("A briefing element of the model response was missing its required interactive controls. Please try again or select another model."),
VisualBriefingValidationRule.MODEL_MARKUP_PROHIBITED => TB("The model response contained markup or code, which this briefing does not allow. Please try again or select another model."), VisualBriefingValidationRule.FORMULA_TARGET_INVALID => TB("A calculation of the model response targeted an invalid briefing element. Please try again or select another model."),
VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), VisualBriefingValidationRule.FORMULA_AST_INVALID => TB("A calculation of the model response used an invalid operation. Please try again or select another model."),
VisualBriefingValidationRule.ACCESSIBILITY_SET_INVALID => TB("The accessibility texts of the model response did not match the briefing elements. Please try again or select another model."),
VisualBriefingValidationRule.ACCESSIBILITY_TEXT_INVALID => TB("An accessibility text of the model response was empty or invalid. Please try again or select another model."),
VisualBriefingValidationRule.LAYOUT_INVALID => TB("The model response used an invalid briefing layout. Please try again or select another model."),
VisualBriefingValidationRule.TEMPLATE_ATTRIBUTE_PROHIBITED => TB("The model response used a prohibited attribute. Please try again or select another model."),
VisualBriefingValidationRule.MODEL_MARKUP_PROHIBITED => TB("The model response contained markup or code, which this briefing does not allow. Please try again or select another model."),
VisualBriefingValidationRule.COMPILER_OUTPUT_INVALID => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."),
_ => string.Empty, _ => string.Empty,
}; };
}
/// <summary>
/// Gets the localized message for one failure code.
/// </summary>
/// <param name="code">The stable failure code.</param> /// <param name="code">The stable failure code.</param>
/// <returns>The localized message.</returns> extension(VisualBriefingFailureCode code)
private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch
{ {
VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."), /// <summary>
VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."), /// Gets the localized message for one failure code.
VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."), /// </summary>
VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."), /// <returns>The localized message.</returns>
VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."), private string ToUserMessage() => code switch
VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED => TB("The sources of this briefing could not be prepared."), {
VisualBriefingFailureCode.PROVIDER_CALL_FAILED => TB("The selected provider could not complete this briefing stage."), VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."),
VisualBriefingFailureCode.RESPONSE_JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."), VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."),
VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID => TB("The model response did not match the required contract. Please try again or select another model."), VisualBriefingFailureCode.MODEL_CAPABILITY_MISSING => TB("The selected model lacks a capability this briefing needs. Please select another model."),
VisualBriefingFailureCode.COMPILER_INVARIANT_VIOLATED => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."), VisualBriefingFailureCode.SOURCE_UNREACHABLE => TB("A source of this briefing can no longer be reached. Please relink or remove the affected source."),
VisualBriefingFailureCode.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."), VisualBriefingFailureCode.TRANSCRIPT_UNAVAILABLE => TB("A media transcript of this briefing is missing or outdated. Please transcribe the affected media again."),
VisualBriefingFailureCode.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."), VisualBriefingFailureCode.SOURCE_PREPARATION_FAILED => TB("The sources of this briefing could not be prepared."),
VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE => TB("The updated content no longer fits the current presentation. You can continue as a rebuild."), VisualBriefingFailureCode.PROVIDER_CALL_FAILED => TB("The selected provider could not complete this briefing stage."),
VisualBriefingFailureCode.PRESENTATION_INVALID => TB("The presentation of the model response did not match the briefing contract. Please try again or select another model."), VisualBriefingFailureCode.RESPONSE_JSON_INVALID => TB("The model did not return valid JSON. Please try again or select another model."),
VisualBriefingFailureCode.ASSEMBLY_FAILED => TB("This briefing could not be assembled."), VisualBriefingFailureCode.RESPONSE_CONTRACT_INVALID => TB("The model response did not match the required contract. Please try again or select another model."),
VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED => TB("The assembled briefing did not pass the security validation."), VisualBriefingFailureCode.COMPILER_INVARIANT_VIOLATED => TB("AI Studio compiled this briefing into an inconsistent result. Please copy the technical details and report this issue."),
VisualBriefingFailureCode.STORE_FAILED => TB("The new version of this briefing could not be saved."), VisualBriefingFailureCode.SOURCE_COVERAGE_INVALID => TB("The model did not cover every source of this briefing exactly once. Please try again or select another model."),
VisualBriefingFailureCode.NO_CHANGES => TB("This operation did not change the briefing, so no new version was created."), VisualBriefingFailureCode.ASSET_PLAN_INVALID => TB("The model did not plan every visual asset of this briefing exactly once. Please try again or select another model."),
VisualBriefingFailureCode.CANCELED => TB("This visual briefing operation was canceled."), VisualBriefingFailureCode.CONTENT_SIGNATURE_INCOMPATIBLE => TB("The updated content no longer fits the current presentation. You can continue as a rebuild."),
VisualBriefingFailureCode.BUILD_INTERRUPTED => TB("AI Studio was closed while this briefing was being built. You can resume the build."), VisualBriefingFailureCode.PRESENTATION_INVALID => TB("The presentation of the model response did not match the briefing contract. Please try again or select another model."),
VisualBriefingFailureCode.UNEXPECTED => TB("This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."), VisualBriefingFailureCode.ASSEMBLY_FAILED => TB("This briefing could not be assembled."),
VisualBriefingFailureCode.ARTIFACT_VALIDATION_FAILED => TB("The assembled briefing did not pass the security validation."),
VisualBriefingFailureCode.STORE_FAILED => TB("The new version of this briefing could not be saved."),
VisualBriefingFailureCode.NO_CHANGES => TB("This operation did not change the briefing, so no new version was created."),
VisualBriefingFailureCode.CANCELED => TB("This visual briefing operation was canceled."),
VisualBriefingFailureCode.BUILD_INTERRUPTED => TB("AI Studio was closed while this briefing was being built. You can resume the build."),
VisualBriefingFailureCode.UNEXPECTED => TB("This visual briefing operation failed because of an unexpected internal error. Please copy the technical details for support."),
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -15,55 +15,58 @@ internal static class VisualBriefingPreviewEndpoint
{ {
private const string ROUTE = "/visual-briefing/preview/{briefingId:guid}/{revisionId:guid}"; private const string ROUTE = "/visual-briefing/preview/{briefingId:guid}/{revisionId:guid}";
/// <summary>
/// Maps the visual briefing preview endpoint.
/// </summary>
/// <param name="app">The web application.</param> /// <param name="app">The web application.</param>
public static void MapVisualBriefingPreview(this WebApplication app) => app.MapGet( extension(WebApplication app)
ROUTE, {
async ( /// <summary>
Guid briefingId, /// Maps the visual briefing preview endpoint.
Guid revisionId, /// </summary>
string? token, public void MapVisualBriefingPreview() => app.MapGet(
HttpContext context, ROUTE,
VisualBriefingPreviewTokenService tokenService, async (
VisualBriefingStore store, Guid briefingId,
ILoggerFactory loggerFactory, Guid revisionId,
CancellationToken cancellationToken) => string? token,
{ HttpContext context,
var logger = loggerFactory.CreateLogger(nameof(VisualBriefingPreviewEndpoint)); VisualBriefingPreviewTokenService tokenService,
if (!tokenService.Validate(token, briefingId, revisionId)) VisualBriefingStore store,
ILoggerFactory loggerFactory,
CancellationToken cancellationToken) =>
{ {
logger.LogWarning( var logger = loggerFactory.CreateLogger(nameof(VisualBriefingPreviewEndpoint));
Event(VisualBriefingLogEventId.PREVIEW_REJECTED), if (!tokenService.Validate(token, briefingId, revisionId))
"Visual briefing preview token rejected. BriefingId={BriefingId} RevisionId={RevisionId}", {
briefingId, logger.LogWarning(
revisionId); Event(VisualBriefingLogEventId.PREVIEW_REJECTED),
"Visual briefing preview token rejected. BriefingId={BriefingId} RevisionId={RevisionId}",
briefingId,
revisionId);
return Results.NotFound(); return Results.NotFound();
} }
// The store re-validates the stored artifact before handing out a stream, so a manually // The store re-validates the stored artifact before handing out a stream, so a manually
// modified file on disk never reaches the preview: // modified file on disk never reaches the preview:
var preview = await store.OpenIntegrityCheckedVersionAsync(briefingId, revisionId, cancellationToken); var preview = await store.OpenIntegrityCheckedVersionAsync(briefingId, revisionId, cancellationToken);
if (preview is null) if (preview is null)
{ {
logger.LogWarning( logger.LogWarning(
Event(VisualBriefingLogEventId.SECURITY_REJECTED), Event(VisualBriefingLogEventId.SECURITY_REJECTED),
"Visual briefing preview artifact rejected. BriefingId={BriefingId} RevisionId={RevisionId}", "Visual briefing preview artifact rejected. BriefingId={BriefingId} RevisionId={RevisionId}",
briefingId, briefingId,
revisionId); revisionId);
return Results.NotFound(); return Results.NotFound();
} }
context.Response.Headers.CacheControl = "no-store"; context.Response.Headers.CacheControl = "no-store";
context.Response.Headers.XContentTypeOptions = "nosniff"; context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers["Referrer-Policy"] = "no-referrer"; context.Response.Headers["Referrer-Policy"] = "no-referrer";
context.Response.Headers.ContentSecurityPolicy = VisualBriefingArtifactService.GetContentSecurityPolicy(preview.Value.Parts); context.Response.Headers.ContentSecurityPolicy = VisualBriefingArtifactService.GetContentSecurityPolicy(preview.Value.Parts);
return Results.File(preview.Value.Stream, "text/html; charset=utf-8", enableRangeProcessing: false); return Results.File(preview.Value.Stream, "text/html; charset=utf-8", enableRangeProcessing: false);
}); });
}
/// <summary> /// <summary>
/// Creates the log event ID for one visual briefing log event. /// Creates the log event ID for one visual briefing log event.
+23 -20
View File
@@ -16,31 +16,34 @@ public static class ChatProvenance
{ {
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(ChatProvenance)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(ChatProvenance));
/// <summary>
/// Collects the entries of the provenance store which a chat refers to.
/// </summary>
/// <remarks>
/// The passages of its answers, the documents attached to its messages, and those attached to
/// the message being written, which may come from a message sent before, e.g., one being edited,
/// together with the texts a preview read of them.
/// </remarks>
/// <param name="thread">The chat.</param> /// <param name="thread">The chat.</param>
/// <returns>The ids of the entries.</returns> extension(ChatThread thread)
public static IReadOnlySet<Guid> CollectProvenanceIds(this ChatThread thread)
{ {
var provenanceIds = new HashSet<Guid>(); /// <summary>
foreach (var block in thread.Blocks) /// Collects the entries of the provenance store which a chat refers to.
/// </summary>
/// <remarks>
/// The passages of its answers, the documents attached to its messages, and those attached to
/// the message being written, which may come from a message sent before, e.g., one being edited,
/// together with the texts a preview read of them.
/// </remarks>
/// <returns>The ids of the entries.</returns>
public IReadOnlySet<Guid> CollectProvenanceIds()
{ {
if (block.Content is ContentText text) var provenanceIds = new HashSet<Guid>();
provenanceIds.UnionWith(text.SourcePassages.Select(passage => passage.ProvenanceId).OfType<Guid>()); foreach (var block in thread.Blocks)
{
if (block.Content is ContentText text)
provenanceIds.UnionWith(text.SourcePassages.Select(passage => passage.ProvenanceId).OfType<Guid>());
if (block.Content is not null) if (block.Content is not null)
provenanceIds.UnionWith(block.Content.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType<Guid>()); provenanceIds.UnionWith(block.Content.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType<Guid>());
}
provenanceIds.UnionWith(thread.Draft.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType<Guid>());
provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId));
return provenanceIds;
} }
provenanceIds.UnionWith(thread.Draft.FileAttachments.Select(attachment => attachment.ProvenanceId).OfType<Guid>());
provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId));
return provenanceIds;
} }
/// <summary> /// <summary>
@@ -6,72 +6,75 @@ namespace AIStudio.Chat;
public static class ChatRestrictionsExtensions public static class ChatRestrictionsExtensions
{ {
/// <summary>
/// Checks if the specified provider may see content with these restrictions.
/// </summary>
/// <remarks>
/// A provider the organization prohibited is never allowed, whatever the restrictions.<br/><br/>
///
/// We don't check if the provider is allowed to use the data sources the content came from.
/// That kind of check is done when the available data sources are resolved. What counts here is
/// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed.
/// </remarks>
/// <param name="restrictions">What the content demands.</param> /// <param name="restrictions">What the content demands.</param>
/// <param name="provider">The provider to check.</param> extension(ChatRestrictions restrictions)
/// <returns>True, when the provider may see the content. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatRestrictions restrictions, T provider)
{ {
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); /// <summary>
/// Checks if the specified provider may see content with these restrictions.
// A provider the organization prohibited is allowed nowhere, not even for content which demands nothing: /// </summary>
if (settingsManager.IsProviderProhibited(GetLLMProvider(provider))) /// <remarks>
return false; /// A provider the organization prohibited is never allowed, whatever the restrictions.<br/><br/>
///
var providerConfidence = provider switch /// We don't check if the provider is allowed to use the data sources the content came from.
/// That kind of check is done when the available data sources are resolved. What counts here is
/// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed.
/// </remarks>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider may see the content. False, otherwise.</returns>
public bool IsLLMProviderAllowed<T>(T provider)
{ {
IProvider p => p.GetConfidenceLevel(settingsManager), var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager),
_ => ConfidenceLevel.UNKNOWN, // A provider the organization prohibited is allowed nowhere, not even for content which demands nothing:
}; if (settingsManager.IsProviderProhibited(GetLLMProvider(provider)))
return false;
// var providerConfidence = provider switch
// The confidence axis is checked on its own: a provider trusted by configuration counts as {
// self-hosted for data-source security, which is the check further down, but that trust IProvider p => p.GetConfidenceLevel(settingsManager),
// says nothing about how confidential the provider is. An organization which wants its AIStudio.Settings.Provider p => p.GetConfidenceLevel(settingsManager),
// contractually covered cloud provider to pass here raises its level through the custom
// confidence scheme instead.
//
if (providerConfidence < restrictions.ProviderConfidence)
return false;
// The data security is not specified. Means, the content never came from RAG, or RAG was _ => ConfidenceLevel.UNKNOWN,
// enabled, but no data sources were selected. That's fine: };
if (restrictions.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
return true;
// //
// Is the provider trusted for data-source security checks? // The confidence axis is checked on its own: a provider trusted by configuration counts as
// // self-hosted for data-source security, which is the check further down, but that trust
var isTrustedProvider = provider switch // says nothing about how confidential the provider is. An organization which wants its
{ // contractually covered cloud provider to pass here raises its level through the custom
IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), // confidence scheme instead.
AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager), //
if (providerConfidence < restrictions.ProviderConfidence)
return false;
_ => false, // The data security is not specified. Means, the content never came from RAG, or RAG was
}; // enabled, but no data sources were selected. That's fine:
if (restrictions.DataSecurity is DataSourceSecurity.NOT_SPECIFIED)
return true;
// //
// Check the data security of the content against the selected provider: // Is the provider trusted for data-source security checks?
// //
return isTrustedProvider switch var isTrustedProvider = provider switch
{ {
// The provider is trusted -- it may see data of any data source: IProvider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
true => true, AIStudio.Settings.Provider p => p.IsTrustedForDataSourceSecurityChecks(settingsManager),
// The provider is not trusted -- it depends on the data security of the content: _ => false,
false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED, };
};
//
// Check the data security of the content against the selected provider:
//
return isTrustedProvider switch
{
// The provider is trusted -- it may see data of any data source:
true => true,
// The provider is not trusted -- it depends on the data security of the content:
false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED,
};
}
} }
private static LLMProviders GetLLMProvider<T>(T provider) => provider switch private static LLMProviders GetLLMProvider<T>(T provider) => provider switch
@@ -6,70 +6,69 @@ public static class ChatRoleExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatRoleExtensions).Namespace, nameof(ChatRoleExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ChatRoleExtensions).Namespace, nameof(ChatRoleExtensions));
/// <summary>
/// Returns the name of the role.
/// </summary>
/// <param name="role">The role.</param> /// <param name="role">The role.</param>
/// <returns>The name of the role.</returns> extension(ChatRole role)
public static string ToName(this ChatRole role) => role switch
{ {
ChatRole.SYSTEM => TB("System"), /// <summary>
ChatRole.USER => TB("You"), /// Returns the name of the role.
ChatRole.AI => TB("AI"), /// </summary>
/// <returns>The name of the role.</returns>
public string ToName() => role switch
{
ChatRole.SYSTEM => TB("System"),
ChatRole.USER => TB("You"),
ChatRole.AI => TB("AI"),
_ => TB("Unknown"), _ => TB("Unknown"),
}; };
/// <summary> /// <summary>
/// Returns the color of the role. /// Returns the color of the role.
/// </summary> /// </summary>
/// <param name="role">The role.</param> /// <returns>The color of the role.</returns>
/// <returns>The color of the role.</returns> public Color ToColor() => role switch
public static Color ToColor(this ChatRole role) => role switch {
{ ChatRole.SYSTEM => Color.Info,
ChatRole.SYSTEM => Color.Info, ChatRole.USER => Color.Primary,
ChatRole.USER => Color.Primary, ChatRole.AI => Color.Tertiary,
ChatRole.AI => Color.Tertiary,
_ => Color.Error, _ => Color.Error,
}; };
/// <summary> /// <summary>
/// Returns the icon of the role. /// Returns the icon of the role.
/// </summary> /// </summary>
/// <param name="role">The role.</param> /// <returns>The icon of the role.</returns>
/// <returns>The icon of the role.</returns> public string ToIcon() => role switch
public static string ToIcon(this ChatRole role) => role switch {
{ ChatRole.SYSTEM => Icons.Material.Filled.Settings,
ChatRole.SYSTEM => Icons.Material.Filled.Settings, ChatRole.USER => Icons.Material.Filled.Person,
ChatRole.USER => Icons.Material.Filled.Person, ChatRole.AI => Icons.Material.Filled.AutoAwesome,
ChatRole.AI => Icons.Material.Filled.AutoAwesome,
_ => Icons.Material.Filled.Help, _ => Icons.Material.Filled.Help,
}; };
/// <summary> /// <summary>
/// Returns the specific name of the role for the chat template. /// Returns the specific name of the role for the chat template.
/// </summary> /// </summary>
/// <param name="role">The role.</param> /// <returns>The name of the role.</returns>
/// <returns>The name of the role.</returns> public string ToChatTemplateName() => role switch
public static string ToChatTemplateName(this ChatRole role) => role switch {
{ ChatRole.SYSTEM => TB("System"),
ChatRole.SYSTEM => TB("System"), ChatRole.USER => TB("User"),
ChatRole.USER => TB("User"), ChatRole.AI => TB("Assistant"),
ChatRole.AI => TB("Assistant"),
_ => TB("Unknown"), _ => TB("Unknown"),
}; };
/// <summary> /// <summary>
/// Selects the next role for a chat template. /// Selects the next role for a chat template.
/// </summary> /// </summary>
/// <param name="currentRole">The current role.</param> /// <returns>The next role for the chat template.</returns>
/// <returns>The next role for the chat template.</returns> public ChatRole SelectNextRoleForTemplate() => role switch
public static ChatRole SelectNextRoleForTemplate(this ChatRole currentRole) => currentRole switch {
{ ChatRole.USER => ChatRole.AI,
ChatRole.USER => ChatRole.AI, _ => ChatRole.USER,
_ => ChatRole.USER, };
}; }
} }
@@ -2,24 +2,27 @@ namespace AIStudio.Chat;
public static class ChatThreadExtensions public static class ChatThreadExtensions
{ {
/// <summary>
/// Checks if the specified provider is allowed for the chat thread.
/// </summary>
/// <remarks>
/// A provider the organization prohibited is never allowed, whatever the chat thread.<br/><br/>
///
/// We don't check if the provider is allowed to use the data sources of the chat thread.
/// That kind of check is done when the available data sources are resolved.<br/><br/>
///
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
/// from the data sources is stored in the chat thread. This means we must check if the
/// selected provider is allowed to use this thread's data security and confidence level.
/// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left
/// the thread is checked with as well.
/// </remarks>
/// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param> /// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param>
/// <param name="provider">The provider to check.</param> extension(ChatThread? chatThread)
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns> {
public static bool IsLLMProviderAllowed<T>(this ChatThread? chatThread, T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider); /// <summary>
/// Checks if the specified provider is allowed for the chat thread.
/// </summary>
/// <remarks>
/// A provider the organization prohibited is never allowed, whatever the chat thread.<br/><br/>
///
/// We don't check if the provider is allowed to use the data sources of the chat thread.
/// That kind of check is done when the available data sources are resolved.<br/><br/>
///
/// One thing which is not so obvious: after RAG was used on this thread, the entire chat
/// thread is kind of a data source by itself. Why? Because the augmentation data collected
/// from the data sources is stored in the chat thread. This means we must check if the
/// selected provider is allowed to use this thread's data security and confidence level.
/// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left
/// the thread is checked with as well.
/// </remarks>
/// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns>
public bool IsLLMProviderAllowed<T>(T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider);
}
} }
@@ -2,114 +2,114 @@ namespace AIStudio.Chat;
public static class IContentExtensions public static class IContentExtensions
{ {
/// <summary> /// <param name="content">The content of a message in the chat.</param>
/// Detaches whoever listens to the stream of this content. extension(IContent content)
/// </summary>
/// <remarks>
/// The streaming handlers are closures over the component which registered them. A content
/// object belongs to the chat thread and therefore outlives every component which renders it,
/// so handlers left behind would keep those components alive for as long as the thread exists.
/// Whoever registers a handler calls this when it is no longer needed.
/// </remarks>
/// <param name="content">The content whose streaming handlers you want to detach.</param>
public static void ResetStreamingHandlers(this IContent content)
{ {
content.StreamingEvent = IContent.NO_STREAMING_HANDLER; /// <summary>
content.StreamingDone = IContent.NO_STREAMING_HANDLER; /// Detaches whoever listens to the stream of this content.
} /// </summary>
/// <remarks>
/// <summary> /// The streaming handlers are closures over the component which registered them. A content
/// Reads this content as the Markdown text the AI produced. /// object belongs to the chat thread and therefore outlives every component which renders it,
/// </summary> /// so handlers left behind would keep those components alive for as long as the thread exists.
/// <remarks> /// Whoever registers a handler calls this when it is no longer needed.
/// Only text content carries Markdown. Everything else, an image for example, has no text /// </remarks>
/// representation at all, which is why this reports failure instead of returning a placeholder: public void ResetStreamingHandlers()
/// a caller which writes files must not put an excuse into the file it writes. This is the text
/// the model wrote and nothing else: whoever reads a table out of a message wants exactly that,
/// while whoever writes a file wants the sources along with it and asks for the export reading.
/// </remarks>
/// <param name="content">The content to read.</param>
/// <param name="markdown">The Markdown text, or an empty string when there is none.</param>
/// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetMarkdownText(this IContent content, out string markdown)
{
if (content is ContentText text)
{ {
markdown = text.Text; content.StreamingEvent = IContent.NO_STREAMING_HANDLER;
return true; content.StreamingDone = IContent.NO_STREAMING_HANDLER;
} }
markdown = string.Empty; /// <summary>
return false; /// Reads this content as the Markdown text the AI produced.
} /// </summary>
/// <remarks>
/// <summary> /// Only text content carries Markdown. Everything else, an image for example, has no text
/// Reads this content the way it leaves AI Studio, as a file or through the clipboard. /// representation at all, which is why this reports failure instead of returning a placeholder:
/// </summary> /// a caller which writes files must not put an excuse into the file it writes. This is the text
/// <remarks> /// the model wrote and nothing else: whoever reads a table out of a message wants exactly that,
/// What the user sees is the answer together with the sources AI Studio collected for it, and /// while whoever writes a file wants the sources along with it and asks for the export reading.
/// that is what a document has to hold as well: an answer built on a web page a tool read, or on /// </remarks>
/// a document of the user, is worth little when the reader cannot tell which one it was. Those /// <param name="markdown">The Markdown text, or an empty string when there is none.</param>
/// sources are not part of the text the model wrote, they hang on the content, which is why /// <returns>True, when this content carries Markdown text.</returns>
/// every path out of the app asks for this and not for the text alone. public bool TryGetMarkdownText(out string markdown)
/// </remarks>
/// <param name="content">The content to read.</param>
/// <param name="markdown">The Markdown text including its sources, or an empty string when there is none.</param>
/// <param name="keepPageAnchors">Whether a link into a local file may name its page. Only a
/// format whose reader stumbles over such a link says no here; the clipboard and every text
/// format keep the page.</param>
/// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetExportMarkdown(this IContent content, out string markdown, bool keepPageAnchors = true)
{
if (content is not ContentText text)
{ {
if (content is ContentText text)
{
markdown = text.Text;
return true;
}
markdown = string.Empty; markdown = string.Empty;
return false; return false;
} }
markdown = AppendSources(text.Text.Trim(), text.Sources.ToExportMarkdown(keepPageAnchors)); /// <summary>
return true; /// Reads this content the way it leaves AI Studio, as a file or through the clipboard.
} /// </summary>
/// <remarks>
/// What the user sees is the answer together with the sources AI Studio collected for it, and
/// that is what a document has to hold as well: an answer built on a web page a tool read, or on
/// a document of the user, is worth little when the reader cannot tell which one it was. Those
/// sources are not part of the text the model wrote, they hang on the content, which is why
/// every path out of the app asks for this and not for the text alone.
/// </remarks>
/// <param name="markdown">The Markdown text including its sources, or an empty string when there is none.</param>
/// <param name="keepPageAnchors">Whether a link into a local file may name its page. Only a
/// format whose reader stumbles over such a link says no here; the clipboard and every text
/// format keep the page.</param>
/// <returns>True, when this content carries Markdown text.</returns>
public bool TryGetExportMarkdown(out string markdown, bool keepPageAnchors = true)
{
if (content is not ContentText text)
{
markdown = string.Empty;
return false;
}
/// <summary> markdown = AppendSources(text.Text.Trim(), text.Sources.ToExportMarkdown(keepPageAnchors));
/// Reads one file out of this content the way it leaves AI Studio, together with the sources return true;
/// the answer rests on. }
/// </summary>
/// <remarks>
/// A document saved on its own came out of the same answer, so it rests on the same sources
/// and takes them along. How depends on the format. Markdown is what the source list is written
/// in, so a Markdown text gets it just as the entire answer does. A web page or a LaTeX document
/// gets it as a comment at its end: anything visible would have to be woven into markup the
/// model wrote. A fragment has no body to put it in, a page may hide whatever lies outside its
/// layout, and one underscore in a title is enough to stop a LaTeX run. A comment breaks
/// neither, and whoever opens the file finds it. A table gets no sources at all, since it has
/// no column a link list would fit into. Neither does code: a program, a script, or a
/// configuration is meant for a compiler or a tool, and a comment of ours would be the one part
/// of it nobody asked for. In some languages, such as JSON, it would even break the file.
///
/// Apart from that, the file is what the model wrote, scripts of a web page included. Saving it
/// is what the user chose to do; the chat still never renders it. Code only gets what its tools
/// expect of a file, see ToSourceCodeFile.
/// </remarks>
/// <param name="content">The content the file was found in.</param>
/// <param name="file">The file, as PlainFileExport.ExtractFiles read it out of this content.</param>
/// <returns>The content of the file to write.</returns>
public static string ToExportContent(this IContent content, MessageFile file)
{
if (file.Format.IsTabular())
return file.Content;
if (file.Format is FileExportFormat.SOURCE_CODE) /// <summary>
return ToSourceCodeFile(file); /// Reads one file out of this content the way it leaves AI Studio, together with the sources
/// the answer rests on.
/// </summary>
/// <remarks>
/// A document saved on its own came out of the same answer, so it rests on the same sources
/// and takes them along. How depends on the format. Markdown is what the source list is written
/// in, so a Markdown text gets it just as the entire answer does. A web page or a LaTeX document
/// gets it as a comment at its end: anything visible would have to be woven into markup the
/// model wrote. A fragment has no body to put it in, a page may hide whatever lies outside its
/// layout, and one underscore in a title is enough to stop a LaTeX run. A comment breaks
/// neither, and whoever opens the file finds it. A table gets no sources at all, since it has
/// no column a link list would fit into. Neither does code: a program, a script, or a
/// configuration is meant for a compiler or a tool, and a comment of ours would be the one part
/// of it nobody asked for. In some languages, such as JSON, it would even break the file.
///
/// Apart from that, the file is what the model wrote, scripts of a web page included. Saving it
/// is what the user chose to do; the chat still never renders it. Code only gets what its tools
/// expect of a file, see ToSourceCodeFile.
/// </remarks>
/// <param name="file">The file, as PlainFileExport.ExtractFiles read it out of this content.</param>
/// <returns>The content of the file to write.</returns>
public string ToExportContent(MessageFile file)
{
if (file.Format.IsTabular())
return file.Content;
var sources = content.Sources.ToExportMarkdown(file.Format.FollowsPageAnchors()); if (file.Format is FileExportFormat.SOURCE_CODE)
if (file.Format is FileExportFormat.MARKDOWN) return ToSourceCodeFile(file);
return AppendSources(file.Content, sources);
if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment)) var sources = content.Sources.ToExportMarkdown(file.Format.FollowsPageAnchors());
return file.Content; if (file.Format is FileExportFormat.MARKDOWN)
return AppendSources(file.Content, sources);
return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; if (sources.Length is 0 || !file.Format.TryToComment(sources, out var comment))
return file.Content;
return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}";
}
} }
/// <summary> /// <summary>
@@ -30,76 +30,79 @@ public static class IImageSourceExtensions
return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES); return Math.Min(Math.Min(engineLimit ?? MAX_IMAGE_SIZE_BYTES, providerLimit ?? MAX_IMAGE_SIZE_BYTES), MAX_IMAGE_SIZE_BYTES);
} }
/// <summary>
/// Read the image content as a base64 string.
/// </summary>
/// <remarks>
/// This is for the images inside the content of a chat, which go to external data sources and
/// agents as they are. Attached images, which go to a model, take ModelImageService instead: it
/// scales them down and encodes them anew within the limit of the provider.<br/>
/// <br/>
/// The images are directly converted to base64 strings. The maximum
/// size of the image is around 10 MB. If the image is larger, the method
/// returns an empty string.<br/>
/// <br/>
/// Note as well that this method returns just the base64 string without
/// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed.
/// </remarks>
/// <param name="image">The image source.</param> /// <param name="image">The image source.</param>
/// <param name="token">The cancellation token.</param> extension(IImageSource image)
/// <returns>The image content as a base64 string; might be empty.</returns>
public static async Task<(bool success, string base64Content)> TryAsBase64(this IImageSource image, CancellationToken token = default)
{ {
switch (image.SourceType) /// <summary>
/// Read the image content as a base64 string.
/// </summary>
/// <remarks>
/// This is for the images inside the content of a chat, which go to external data sources and
/// agents as they are. Attached images, which go to a model, take ModelImageService instead: it
/// scales them down and encodes them anew within the limit of the provider.<br/>
/// <br/>
/// The images are directly converted to base64 strings. The maximum
/// size of the image is around 10 MB. If the image is larger, the method
/// returns an empty string.<br/>
/// <br/>
/// Note as well that this method returns just the base64 string without
/// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed.
/// </remarks>
/// <param name="token">The cancellation token.</param>
/// <returns>The image content as a base64 string; might be empty.</returns>
public async Task<(bool success, string base64Content)> TryAsBase64(CancellationToken token = default)
{ {
case ContentImageSource.BASE64: switch (image.SourceType)
return (success: true, image.Source);
case ContentImageSource.URL:
{ {
using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED); case ContentImageSource.BASE64:
using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token); return (success: true, image.Source);
var timeoutToken = timeoutTokenSource.Token;
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken); case ContentImageSource.URL:
if(response.IsSuccessStatusCode)
{ {
// Read the length of the content: using var httpClient = ExternalHttpClientTimeout.CreateHttpClient(ExternalHttpTrustPolicy.ALLOW_CUSTOM_ROOTS_WHEN_HOST_WHITELISTED);
var lengthBytes = response.Content.Headers.ContentLength; using var timeoutTokenSource = ExternalHttpClientTimeout.CreateTimeoutTokenSource(token);
if(lengthBytes > MAX_IMAGE_SIZE_BYTES) var timeoutToken = timeoutTokenSource.Token;
using var response = await httpClient.GetAsync(image.Source, HttpCompletionOption.ResponseHeadersRead, timeoutToken);
if(response.IsSuccessStatusCode)
{ {
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image."))); // Read the length of the content:
return (success: false, string.Empty); var lengthBytes = response.Content.Headers.ContentLength;
if(lengthBytes > MAX_IMAGE_SIZE_BYTES)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The image at the URL is too large (>10 MB). Skipping the image.")));
return (success: false, string.Empty);
}
var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken);
return (success: true, Convert.ToBase64String(bytes));
} }
var bytes = await response.Content.ReadAsByteArrayAsync(timeoutToken); await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("Failed to download the image from the URL. Skipping the image.")));
return (success: true, Convert.ToBase64String(bytes)); return (success: false, string.Empty);
} }
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("Failed to download the image from the URL. Skipping the image."))); case ContentImageSource.LOCAL_PATH:
return (success: false, string.Empty); if(File.Exists(image.Source))
{
// Read the content length:
var length = new FileInfo(image.Source).Length;
if(length > MAX_IMAGE_SIZE_BYTES)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image.")));
return (success: false, string.Empty);
}
var bytes = await File.ReadAllBytesAsync(image.Source, token);
return (success: true, Convert.ToBase64String(bytes));
}
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file does not exist. Skipping the image.")));
return (success: false, string.Empty);
default:
return (success: false, string.Empty);
} }
case ContentImageSource.LOCAL_PATH:
if(File.Exists(image.Source))
{
// Read the content length:
var length = new FileInfo(image.Source).Length;
if(length > MAX_IMAGE_SIZE_BYTES)
{
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file is too large (>10 MB). Skipping the image.")));
return (success: false, string.Empty);
}
var bytes = await File.ReadAllBytesAsync(image.Source, token);
return (success: true, Convert.ToBase64String(bytes));
}
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, TB("The local image file does not exist. Skipping the image.")));
return (success: false, string.Empty);
default:
return (success: false, string.Empty);
} }
} }
} }
@@ -9,161 +9,159 @@ namespace AIStudio.Chat;
public static class ListContentBlockExtensions public static class ListContentBlockExtensions
{ {
/// <summary>
/// Processes a list of content blocks by transforming them into a collection of message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param> /// <param name="blocks">The list of content blocks to process.</param>
/// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param> extension(List<ContentBlock> blocks)
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
/// <param name="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
/// <param name="token">The cancellation token.</param>
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
public static async Task<IList<IMessageBase>> BuildMessagesAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider,
Func<ChatRole, string> roleTransformer,
Func<string, ISubContent> textSubContentFactory,
Func<PreparedModelImage, ISubContent> imageSubContentFactory,
CancellationToken token = default)
{ {
// /// <summary>
// Asked through the configured provider, so that what a person set in their expert settings /// Processes a list of content blocks by transforming them into a collection of message results asynchronously.
// counts here too. It did not: this path read the automatic answer alone, so somebody who /// </summary>
// switched image input on saw it work while attaching the picture and saw it ignored while /// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
// the message was built -- every chat round and every tool round. /// <param name="provider">The configured provider, whose model is being written to.</param>
// /// <param name="textSubContentFactory">A factory function to create text sub-content.</param>
var canProcessImages = provider.SupportsImageInput(); /// <param name="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
/// <param name="token">The cancellation token.</param>
// /// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
// One report for the whole request: every message of the conversation reads its attached public async Task<IList<IMessageBase>> BuildMessagesAsync(
// documents anew, all at the same time. Twenty documents must produce one dialog listing AIStudio.Settings.Provider provider,
// all of them, not twenty dialogs in a row. A request without documents has nothing to Func<ChatRole, string> roleTransformer,
// report, and needs no scope. Func<string, ISubContent> textSubContentFactory,
// Func<PreparedModelImage, ISubContent> imageSubContentFactory,
await using var reportingScope = blocks.Any(block => block.Content is ContentText text && text.FileAttachments.MayContainDocuments()) CancellationToken token = default)
? Program.SERVICE_PROVIDER.GetRequiredService<PromptInjectionGuardService>().BeginAction()
: null;
var messageTaskList = new List<Task<IMessageBase>>(blocks.Count);
foreach (var block in blocks)
{ {
switch (block.Content) //
// Asked through the configured provider, so that what a person set in their expert settings
// counts here too. It did not: this path read the automatic answer alone, so somebody who
// switched image input on saw it work while attaching the picture and saw it ignored while
// the message was built -- every chat round and every tool round.
//
var canProcessImages = provider.SupportsImageInput();
//
// One report for the whole request: every message of the conversation reads its attached
// documents anew, all at the same time. Twenty documents must produce one dialog listing
// all of them, not twenty dialogs in a row. A request without documents has nothing to
// report, and needs no scope.
//
await using var reportingScope = blocks.Any(block => block.Content is ContentText text && text.FileAttachments.MayContainDocuments())
? Program.SERVICE_PROVIDER.GetRequiredService<PromptInjectionGuardService>().BeginAction()
: null;
var messageTaskList = new List<Task<IMessageBase>>(blocks.Count);
foreach (var block in blocks)
{ {
// The prompt may or may not contain image(s), but the provider/model cannot process images. switch (block.Content)
// Thus, we treat it as a regular text message. {
case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !canProcessImages: // The prompt may or may not contain image(s), but the provider/model cannot process images.
messageTaskList.Add(CreateTextMessageAsync(block, text)); // Thus, we treat it as a regular text message.
break; case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !canProcessImages:
messageTaskList.Add(CreateTextMessageAsync(block, text));
break;
// The regular case for text content without images: // The regular case for text content without images:
case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !text.FileAttachments.ContainsImages(): case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && !text.FileAttachments.ContainsImages():
messageTaskList.Add(CreateTextMessageAsync(block, text)); messageTaskList.Add(CreateTextMessageAsync(block, text));
break; break;
// Text prompt with images as attachments, and the provider/model can process images: // Text prompt with images as attachments, and the provider/model can process images:
case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages(): case ContentText text when block.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace(text.Text) && text.FileAttachments.ContainsImages():
messageTaskList.Add(CreateMultimodalMessageAsync(block, text)); messageTaskList.Add(CreateMultimodalMessageAsync(block, text));
break; break;
}
}
// Await all messages:
await Task.WhenAll(messageTaskList);
// Select all results:
return messageTaskList.Select(n => n.Result).ToList();
// Local function to create a text message asynchronously.
Task<IMessageBase> CreateTextMessageAsync(ContentBlock block, ContentText text)
{
return Task.Run(async () => new TextMessage
{
Role = roleTransformer(block.Role),
Content = await text.PrepareTextContentForAI(reportingScope),
} as IMessageBase);
}
// Local function to create a multimodal message asynchronously.
Task<IMessageBase> CreateMultimodalMessageAsync(ContentBlock block, ContentText text)
{
return Task.Run(async () =>
{
var images = await Task.WhenAll(text.FileAttachments
.Where(x => x is { IsImage: true, Exists: true })
.Cast<FileAttachmentImage>()
.Select(CreateImageSubContentAsync));
return new MultimodalMessage
{
Role = roleTransformer(block.Role),
Content =
[
textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)),
..images.OfType<ISubContent>(),
]
} as IMessageBase;
});
}
//
// Local function to prepare one image for the model. An image which cannot be prepared
// stays out of the message, and the user learns why. It used to go along as an image
// without data, which the providers refuse together with the whole request.
//
async Task<ISubContent?> CreateImageSubContentAsync(FileAttachmentImage image)
{
var prepared = await Program.SERVICE_PROVIDER.GetRequiredService<ModelImageService>().PrepareAsync(image, provider, token);
if (prepared.IsPrepared)
return imageSubContentFactory(prepared);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName)));
return null;
} }
} }
// Await all messages: /// <summary>
await Task.WhenAll(messageTaskList); /// Processes a list of content blocks using direct image URL format to create message results asynchronously.
/// </summary>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks>
/// Uses direct image URL format where the image data is placed directly in the image_url field:
/// <code>
/// { "type": "image_url", "image_url": "data:image/jpeg;base64,..." }
/// </code>
/// This format is used by OpenAI, Mistral, and Ollama.
/// </remarks>
public async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider,
StandardRoleTransformer,
StandardTextSubContentFactory,
DirectImageSubContentFactory);
// Select all results: /// <summary>
return messageTaskList.Select(n => n.Result).ToList(); /// Processes a list of content blocks using nested image URL format to create message results asynchronously.
/// </summary>
// Local function to create a text message asynchronously. /// <param name="provider">The configured provider, whose model is being written to.</param>
Task<IMessageBase> CreateTextMessageAsync(ContentBlock block, ContentText text) /// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
{ /// <remarks>
return Task.Run(async () => new TextMessage /// Uses nested image URL format where the image data is wrapped in an object:
{ /// <code>
Role = roleTransformer(block.Role), /// { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
Content = await text.PrepareTextContentForAI(reportingScope), /// </code>
} as IMessageBase); /// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers.
} /// </remarks>
public async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
// Local function to create a multimodal message asynchronously. AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
Task<IMessageBase> CreateMultimodalMessageAsync(ContentBlock block, ContentText text) provider,
{ StandardRoleTransformer,
return Task.Run(async () => StandardTextSubContentFactory,
{ NestedImageSubContentFactory);
var images = await Task.WhenAll(text.FileAttachments
.Where(x => x is { IsImage: true, Exists: true })
.Cast<FileAttachmentImage>()
.Select(CreateImageSubContentAsync));
return new MultimodalMessage
{
Role = roleTransformer(block.Role),
Content =
[
textSubContentFactory(await text.PrepareTextContentForAI(reportingScope)),
..images.OfType<ISubContent>(),
]
} as IMessageBase;
});
}
//
// Local function to prepare one image for the model. An image which cannot be prepared
// stays out of the message, and the user learns why. It used to go along as an image
// without data, which the providers refuse together with the whole request.
//
async Task<ISubContent?> CreateImageSubContentAsync(FileAttachmentImage image)
{
var prepared = await Program.SERVICE_PROVIDER.GetRequiredService<ModelImageService>().PrepareAsync(image, provider, token);
if (prepared.IsPrepared)
return imageSubContentFactory(prepared);
await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.ImageNotSupported, prepared.ErrorCode.ToUserMessage(image.FileName)));
return null;
}
} }
/// <summary>
/// Processes a list of content blocks using direct image URL format to create message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks>
/// Uses direct image URL format where the image data is placed directly in the image_url field:
/// <code>
/// { "type": "image_url", "image_url": "data:image/jpeg;base64,..." }
/// </code>
/// This format is used by OpenAI, Mistral, and Ollama.
/// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider,
StandardRoleTransformer,
StandardTextSubContentFactory,
DirectImageSubContentFactory);
/// <summary>
/// Processes a list of content blocks using nested image URL format to create message results asynchronously.
/// </summary>
/// <param name="blocks">The list of content blocks to process.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param>
/// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks>
/// Uses nested image URL format where the image data is wrapped in an object:
/// <code>
/// { "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,..." } }
/// </code>
/// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers.
/// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider,
StandardRoleTransformer,
StandardTextSubContentFactory,
NestedImageSubContentFactory);
private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText
{ {
Text = text, Text = text,
@@ -2,17 +2,20 @@ namespace AIStudio.Chat;
public static class ListFileAttachmentExtensions public static class ListFileAttachmentExtensions
{ {
public static bool ContainsImages(this List<FileAttachment> attachments) => attachments.Any(attachment => attachment.IsImage);
/// <summary>
/// Whether preparing the message for the AI may read a document through the runtime.
/// </summary>
/// <remarks>
/// Every attachment but a picture counts: its type is checked again with the latest rules
/// before it is read, cf. FileAttachment.Normalize, and may turn out to be a document then.
/// Checking that here would touch every file twice, which takes long on a network drive.
/// </remarks>
/// <param name="attachments">The attachments of the message.</param> /// <param name="attachments">The attachments of the message.</param>
/// <returns>True when a document may be read.</returns> extension(List<FileAttachment> attachments)
public static bool MayContainDocuments(this List<FileAttachment> attachments) => attachments.Any(attachment => !attachment.IsImage); {
public bool ContainsImages() => attachments.Any(attachment => attachment.IsImage);
/// <summary>
/// Whether preparing the message for the AI may read a document through the runtime.
/// </summary>
/// <remarks>
/// Every attachment but a picture counts: its type is checked again with the latest rules
/// before it is read, cf. FileAttachment.Normalize, and may turn out to be a document then.
/// Checking that here would touch every file twice, which takes long on a network drive.
/// </remarks>
/// <returns>True when a document may be read.</returns>
public bool MayContainDocuments() => attachments.Any(attachment => !attachment.IsImage);
}
} }
@@ -2,20 +2,23 @@ namespace AIStudio.Chat;
public static class SetFileAttachmentExtensions public static class SetFileAttachmentExtensions
{ {
/// <summary>
/// Attaches a file in place of the attachment of the same file the set held before.
/// </summary>
/// <remarks>
/// The attachments differ by more than their files: one of a message being edited holds the
/// entry of the text it was sent with, and the size of the file may have changed since. Both
/// would stay in the set, and the message would go with the old and the new text of the file.
/// Attaching a file again means the file as it is now.
/// </remarks>
/// <param name="attachments">The attachments of a message.</param> /// <param name="attachments">The attachments of a message.</param>
/// <param name="attachment">The attachment of the file just attached.</param> extension(HashSet<FileAttachment> attachments)
public static void AttachInPlaceOfSameFile(this HashSet<FileAttachment> attachments, FileAttachment attachment)
{ {
attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal)); /// <summary>
attachments.Add(attachment); /// Attaches a file in place of the attachment of the same file the set held before.
/// </summary>
/// <remarks>
/// The attachments differ by more than their files: one of a message being edited holds the
/// entry of the text it was sent with, and the size of the file may have changed since. Both
/// would stay in the set, and the message would go with the old and the new text of the file.
/// Attaching a file again means the file as it is now.
/// </remarks>
/// <param name="attachment">The attachment of the file just attached.</param>
public void AttachInPlaceOfSameFile(FileAttachment attachment)
{
attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal));
attachments.Add(attachment);
}
} }
} }
+32 -29
View File
@@ -2,37 +2,40 @@
public static class StringExtensions public static class StringExtensions
{ {
public static string RemoveThinkTags(this string input)
{
const string OPEN_TAG = "<think>";
const string CLOSE_TAG = "</think>";
if (string.IsNullOrWhiteSpace(input) || !input.StartsWith(OPEN_TAG, StringComparison.Ordinal))
return input;
var endIndex = input.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
if (endIndex == -1)
return string.Empty;
return input[(endIndex + CLOSE_TAG.Length)..];
}
/// <summary>
/// Shortens a text to the given number of characters, and marks the cut with three dots.
/// </summary>
/// <remarks>
/// Never cuts between the two halves of a surrogate pair, which no JSON writer takes.<br/><br/>
/// Takes and returns a string rather than a span on purpose: most texts are short enough, and
/// those come back as the very same instance. Only a text which is cut costs one new string.
/// </remarks>
/// <param name="text">The text.</param> /// <param name="text">The text.</param>
/// <param name="maxCharacters">How many characters of the text to keep at most, the dots not counted.</param> extension(string text)
/// <returns>The text as it was when it is short enough, otherwise its beginning followed by three dots.</returns>
public static string Shorten(this string text, int maxCharacters)
{ {
if (text.Length <= maxCharacters) public string RemoveThinkTags()
return text; {
const string OPEN_TAG = "<think>";
const string CLOSE_TAG = "</think>";
if (string.IsNullOrWhiteSpace(text) || !text.StartsWith(OPEN_TAG, StringComparison.Ordinal))
return text;
var end = char.IsHighSurrogate(text[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters; var endIndex = text.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
return string.Concat(text.AsSpan(0, end).TrimEnd(), "..."); if (endIndex == -1)
return string.Empty;
return text[(endIndex + CLOSE_TAG.Length)..];
}
/// <summary>
/// Shortens a text to the given number of characters, and marks the cut with three dots.
/// </summary>
/// <remarks>
/// Never cuts between the two halves of a surrogate pair, which no JSON writer takes.<br/><br/>
/// Takes and returns a string rather than a span on purpose: most texts are short enough, and
/// those come back as the very same instance. Only a text which is cut costs one new string.
/// </remarks>
/// <param name="maxCharacters">How many characters of the text to keep at most, the dots not counted.</param>
/// <returns>The text as it was when it is short enough, otherwise its beginning followed by three dots.</returns>
public string Shorten(int maxCharacters)
{
if (text.Length <= maxCharacters)
return text;
var end = char.IsHighSurrogate(text[maxCharacters - 1]) ? maxCharacters - 1 : maxCharacters;
return string.Concat(text.AsSpan(0, end).TrimEnd(), "...");
}
} }
} }
@@ -6,19 +6,22 @@ public static class ReadWebContentStepsExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReadWebContentStepsExtensions).Namespace, nameof(ReadWebContentStepsExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ReadWebContentStepsExtensions).Namespace, nameof(ReadWebContentStepsExtensions));
/// <summary>
/// Gets the text representation of a given ReadWebContentSteps enum value.
/// </summary>
/// <param name="step">The ReadWebContentSteps enum value.</param> /// <param name="step">The ReadWebContentSteps enum value.</param>
/// <returns>>The text representation of the process step.</returns> extension(ReadWebContentSteps step)
public static string GetText(this ReadWebContentSteps step) => step switch
{ {
ReadWebContentSteps.START => TB("Start"), /// <summary>
ReadWebContentSteps.LOADING => TB("Loading"), /// Gets the text representation of a given ReadWebContentSteps enum value.
ReadWebContentSteps.PARSING => TB("Parsing"), /// </summary>
ReadWebContentSteps.CLEANING => TB("Cleaning"), /// <returns>>The text representation of the process step.</returns>
ReadWebContentSteps.DONE => TB("Done"), public string GetText() => step switch
{
ReadWebContentSteps.START => TB("Start"),
ReadWebContentSteps.LOADING => TB("Loading"),
ReadWebContentSteps.PARSING => TB("Parsing"),
ReadWebContentSteps.CLEANING => TB("Cleaning"),
ReadWebContentSteps.DONE => TB("Done"),
_ => TB("n/a") _ => TB("n/a")
}; };
}
} }
@@ -7,50 +7,53 @@ public static class ConfidenceLevelExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceLevelExtensions).Namespace, nameof(ConfidenceLevelExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceLevelExtensions).Namespace, nameof(ConfidenceLevelExtensions));
public static string GetName(this ConfidenceLevel level) => level switch extension(ConfidenceLevel level)
{ {
ConfidenceLevel.NONE => TB("No provider selected"), public string GetName() => level switch
{
ConfidenceLevel.NONE => TB("No provider selected"),
ConfidenceLevel.UNTRUSTED => TB("Untrusted"), ConfidenceLevel.UNTRUSTED => TB("Untrusted"),
ConfidenceLevel.UNKNOWN => TB("Unknown"), ConfidenceLevel.UNKNOWN => TB("Unknown"),
ConfidenceLevel.VERY_LOW => TB("Very Low"), ConfidenceLevel.VERY_LOW => TB("Very Low"),
ConfidenceLevel.LOW => TB("Low"), ConfidenceLevel.LOW => TB("Low"),
ConfidenceLevel.MODERATE => TB("Moderate"), ConfidenceLevel.MODERATE => TB("Moderate"),
ConfidenceLevel.MEDIUM => TB("Medium"), ConfidenceLevel.MEDIUM => TB("Medium"),
ConfidenceLevel.HIGH => TB("High"), ConfidenceLevel.HIGH => TB("High"),
_ => TB("Unknown confidence level"), _ => TB("Unknown confidence level"),
}; };
public static string GetColor(this ConfidenceLevel level, SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch public string GetColor(SettingsManager settingsManager) => (level, settingsManager.IsDarkMode) switch
{ {
(ConfidenceLevel.NONE, false) => "#cccccc", (ConfidenceLevel.NONE, false) => "#cccccc",
(ConfidenceLevel.NONE, true) => "#666666", (ConfidenceLevel.NONE, true) => "#666666",
(ConfidenceLevel.UNKNOWN, false) => "#777777", (ConfidenceLevel.UNKNOWN, false) => "#777777",
(ConfidenceLevel.UNKNOWN, true) => "#aaaaaa", (ConfidenceLevel.UNKNOWN, true) => "#aaaaaa",
(ConfidenceLevel.UNTRUSTED, false) => "#ff0000", (ConfidenceLevel.UNTRUSTED, false) => "#ff0000",
(ConfidenceLevel.UNTRUSTED, true) => "#800000", (ConfidenceLevel.UNTRUSTED, true) => "#800000",
(ConfidenceLevel.VERY_LOW, false) => "#ff6600", (ConfidenceLevel.VERY_LOW, false) => "#ff6600",
(ConfidenceLevel.VERY_LOW, true) => "#803300", (ConfidenceLevel.VERY_LOW, true) => "#803300",
(ConfidenceLevel.LOW, false) => "#ffcc00", (ConfidenceLevel.LOW, false) => "#ffcc00",
(ConfidenceLevel.LOW, true) => "#806600", (ConfidenceLevel.LOW, true) => "#806600",
(ConfidenceLevel.MODERATE, false) => "#99cc00", (ConfidenceLevel.MODERATE, false) => "#99cc00",
(ConfidenceLevel.MODERATE, true) => "#4d6600", (ConfidenceLevel.MODERATE, true) => "#4d6600",
(ConfidenceLevel.MEDIUM, false) => "#86b300", (ConfidenceLevel.MEDIUM, false) => "#86b300",
(ConfidenceLevel.MEDIUM, true) => "#394d00", (ConfidenceLevel.MEDIUM, true) => "#394d00",
(ConfidenceLevel.HIGH, false) => "#009933", (ConfidenceLevel.HIGH, false) => "#009933",
(ConfidenceLevel.HIGH, true) => "#004d1a", (ConfidenceLevel.HIGH, true) => "#004d1a",
(_, false) => "#cc6600", (_, false) => "#cc6600",
(_, true) => "#663300", (_, true) => "#663300",
}; };
public static string SetColorStyle(this ConfidenceLevel level, SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};"; public string SetColorStyle(SettingsManager settingsManager) => $"--confidence-color: {level.GetColor(settingsManager)};";
}
} }
@@ -6,153 +6,153 @@ public static class HFInferenceProviderExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(HFInferenceProviderExtensions).Namespace, nameof(HFInferenceProviderExtensions));
/// <summary>
/// The slug Hugging Face uses for this inference provider.
/// </summary>
/// <param name="provider">The inference provider.</param> /// <param name="provider">The inference provider.</param>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns> extension(HFInferenceProvider provider)
public static string EndpointsId(this HFInferenceProvider provider) => provider switch
{ {
HFInferenceProvider.BASETEN => "baseten", /// <summary>
HFInferenceProvider.CEREBRAS => "cerebras", /// The slug Hugging Face uses for this inference provider.
HFInferenceProvider.COHERE => "cohere", /// </summary>
HFInferenceProvider.DEEPINFRA => "deepinfra", /// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
HFInferenceProvider.FEATHERLESS_AI => "featherless-ai", public string EndpointsId() => provider switch
HFInferenceProvider.FIREWORKS => "fireworks-ai", {
HFInferenceProvider.GROQ => "groq", HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.NOVITA => "novita", HFInferenceProvider.CEREBRAS => "cerebras",
HFInferenceProvider.NSCALE => "nscale", HFInferenceProvider.COHERE => "cohere",
HFInferenceProvider.OVHCLOUD => "ovhcloud", HFInferenceProvider.DEEPINFRA => "deepinfra",
HFInferenceProvider.PUBLIC_AI => "publicai", HFInferenceProvider.FEATHERLESS_AI => "featherless-ai",
HFInferenceProvider.SCALEWAY => "scaleway", HFInferenceProvider.FIREWORKS => "fireworks-ai",
HFInferenceProvider.TOGETHER_AI => "together", HFInferenceProvider.GROQ => "groq",
HFInferenceProvider.ZAI => "zai-org", HFInferenceProvider.NOVITA => "novita",
HFInferenceProvider.NSCALE => "nscale",
HFInferenceProvider.OVHCLOUD => "ovhcloud",
HFInferenceProvider.PUBLIC_AI => "publicai",
HFInferenceProvider.SCALEWAY => "scaleway",
HFInferenceProvider.TOGETHER_AI => "together",
HFInferenceProvider.ZAI => "zai-org",
_ => string.Empty, _ => string.Empty,
}; };
/// <summary> /// <summary>
/// The suffix which tells the router where to send the request. /// The suffix which tells the router where to send the request.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The router serves every provider through one endpoint. Which provider answers is decided by /// The router serves every provider through one endpoint. Which provider answers is decided by
/// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router /// a suffix on the model name, e.g. "google/gemma-4-31B-it:novita". Without a suffix, the router
/// picks the fastest provider itself. /// picks the fastest provider itself.
/// </remarks> /// </remarks>
/// <param name="provider">The inference provider.</param> /// <returns>The suffix including its colon, or an empty string when the router should choose.</returns>
/// <returns>The suffix including its colon, or an empty string when the router should choose.</returns> public string ModelSuffix() => provider switch
public static string ModelSuffix(this HFInferenceProvider provider) => provider switch {
{ HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
HFInferenceProvider.CHEAPEST => ":cheapest", HFInferenceProvider.CHEAPEST => ":cheapest",
HFInferenceProvider.PREFERRED => ":preferred", HFInferenceProvider.PREFERRED => ":preferred",
_ => $":{provider.EndpointsId()}", _ => $":{provider.EndpointsId()}",
}; };
/// <summary> /// <summary>
/// Whether this inference provider serves models to chat with. /// Whether this inference provider serves models to chat with.
/// </summary> /// </summary>
/// <param name="provider">The inference provider.</param> /// <returns>True, when the provider serves chat models.</returns>
/// <returns>True, when the provider serves chat models.</returns> public bool SupportsChat() => provider is not HFInferenceProvider.NONE;
public static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE;
/// <summary> /// <summary>
/// Whether this inference provider creates embeddings for us. /// Whether this inference provider creates embeddings for us.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Embeddings are a much shorter story than chatting. The router serves them nowhere near its /// Embeddings are a much shorter story than chatting. The router serves them nowhere near its
/// own endpoint, only through the route of a provider, and only two of those answer the /// own endpoint, only through the route of a provider, and only two of those answer the
/// OpenAI-compatible form we send. The routing strategies are out by their nature: without a /// OpenAI-compatible form we send. The routing strategies are out by their nature: without a
/// named provider there is no route to address. /// named provider there is no route to address.
/// </remarks> /// </remarks>
/// <param name="provider">The inference provider.</param> /// <returns>True, when we can create embeddings through this provider.</returns>
/// <returns>True, when we can create embeddings through this provider.</returns> public bool SupportsEmbeddings() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
public static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary> /// <summary>
/// Whether this inference provider transcribes audio for us. /// Whether this inference provider transcribes audio for us.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The same two providers as for embeddings, and for the same reason: transcription lives on a /// The same two providers as for embeddings, and for the same reason: transcription lives on a
/// provider's own route, and only these two answer the OpenAI-compatible form there. Others do /// provider's own route, and only these two answer the OpenAI-compatible form there. Others do
/// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and /// transcribe for Hugging Face, but not in a shape we could send an audio file to: fal-ai and
/// Replicate both turn the request down with "Model not supported by provider". /// Replicate both turn the request down with "Model not supported by provider".
/// </remarks> /// </remarks>
/// <param name="provider">The inference provider.</param> /// <returns>True, when we can transcribe audio through this provider.</returns>
/// <returns>True, when we can transcribe audio through this provider.</returns> public bool SupportsTranscription() => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
public static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA;
/// <summary> /// <summary>
/// The base URL of the provider's own OpenAI-compatible route. /// The base URL of the provider's own OpenAI-compatible route.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Only chatting goes through the router's own endpoint. Everything else has to address the /// Only chatting goes through the router's own endpoint. Everything else has to address the
/// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra /// provider directly, and they do not agree on where their OpenAI-compatible API sits: DeepInfra
/// keeps it below an additional "openai" segment, and answers the path without it with /// keeps it below an additional "openai" segment, and answers the path without it with
/// "Not allowed to POST /v1/embeddings for provider deepinfra". /// "Not allowed to POST /v1/embeddings for provider deepinfra".
/// </remarks> /// </remarks>
/// <param name="provider">The inference provider.</param> /// <returns>The base URL, or an empty string when the provider has no such route.</returns>
/// <returns>The base URL, or an empty string when the provider has no such route.</returns> public string ProviderBaseURL() => provider switch
public static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch {
{ HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/",
HFInferenceProvider.TOGETHER_AI => "https://router.huggingface.co/together/v1/", HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
HFInferenceProvider.DEEPINFRA => "https://router.huggingface.co/deepinfra/v1/openai/",
_ => string.Empty, _ => string.Empty,
}; };
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public string CatalogFilter()
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public string ToName() => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"),
HFInferenceProvider.BASETEN => "Baseten",
HFInferenceProvider.CEREBRAS => "Cerebras",
HFInferenceProvider.COHERE => "Cohere",
HFInferenceProvider.DEEPINFRA => "DeepInfra",
HFInferenceProvider.FEATHERLESS_AI => "Featherless AI",
HFInferenceProvider.FIREWORKS => "Fireworks AI",
HFInferenceProvider.GROQ => "Groq",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.NSCALE => "Nscale",
HFInferenceProvider.OVHCLOUD => "OVHcloud",
HFInferenceProvider.PUBLIC_AI => "Public AI",
HFInferenceProvider.SCALEWAY => "Scaleway",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.ZAI => "Z.ai",
_ => string.Empty,
};
}
/// <summary>
/// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// The suffix says where a request goes, not what the model is. Everything asking what a model
/// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as
/// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it.
/// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last
/// colon always starts the suffix.
/// </remarks>
/// <param name="model">The model as it is configured.</param> /// <param name="model">The model as it is configured.</param>
/// <returns>The model without its routing suffix.</returns> extension(Model model)
public static Model WithoutRoutingSuffix(this Model model)
{ {
var separatorIndex = model.Id.LastIndexOf(':'); /// <summary>
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] }; /// Removes the routing suffix from a model, if it carries one.
/// </summary>
/// <remarks>
/// The suffix says where a request goes, not what the model is. Everything asking what a model
/// can do has to look at the bare name: "google/gemma-4-31B-it:novita" is the same model as
/// "google/gemma-4-31B-it", and a name detection which never heard of the suffix would miss it.
/// Model IDs on the hub are written as "org/model" and carry no colon of their own, so the last
/// colon always starts the suffix.
/// </remarks>
/// <returns>The model without its routing suffix.</returns>
public Model WithoutRoutingSuffix()
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
} }
/// <summary>
/// The value to filter the Hugging Face model catalog by.
/// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public static string CatalogFilter(this HFInferenceProvider provider)
{
var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug;
}
public static string ToName(this HFInferenceProvider provider) => provider switch
{
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
HFInferenceProvider.PREFERRED => TB("Automatic: your preferred order"),
HFInferenceProvider.BASETEN => "Baseten",
HFInferenceProvider.CEREBRAS => "Cerebras",
HFInferenceProvider.COHERE => "Cohere",
HFInferenceProvider.DEEPINFRA => "DeepInfra",
HFInferenceProvider.FEATHERLESS_AI => "Featherless AI",
HFInferenceProvider.FIREWORKS => "Fireworks AI",
HFInferenceProvider.GROQ => "Groq",
HFInferenceProvider.NOVITA => "Novita",
HFInferenceProvider.NSCALE => "Nscale",
HFInferenceProvider.OVHCLOUD => "OVHcloud",
HFInferenceProvider.PUBLIC_AI => "Public AI",
HFInferenceProvider.SCALEWAY => "Scaleway",
HFInferenceProvider.TOGETHER_AI => "Together AI",
HFInferenceProvider.ZAI => "Z.ai",
_ => string.Empty,
};
} }
File diff suppressed because it is too large. Load diff
@@ -5,41 +5,47 @@ public static class LLMProvidersIconExtensions
private const string ICON_ROOT = "/images/provider-icons"; private const string ICON_ROOT = "/images/provider-icons";
private const string SVG_DATA_URL_PREFIX = "data:image/svg+xml;base64,"; private const string SVG_DATA_URL_PREFIX = "data:image/svg+xml;base64,";
public static string GetIconUrl(this AIStudio.Settings.Provider provider, bool isDarkMode) extension(AIStudio.Settings.Provider provider)
=> provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl)
{ {
if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true) public string GetIconUrl(bool isDarkMode)
return customIconDataUrl; => provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
return provider.GetIconUrl(isDarkMode);
} }
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch extension(LLMProviders provider)
{ {
LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", public string GetIconUrl(bool isDarkMode, string? customIconDataUrl)
LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg", {
LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg", if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true)
LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg", return customIconDataUrl;
LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg",
LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg", return provider.GetIconUrl(isDarkMode);
LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg", }
LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg",
LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg", public string GetIconUrl(bool isDarkMode) => provider switch
LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg", {
LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg", LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg", LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg",
LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg", LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg",
LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg", LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg",
LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg", LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg",
LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg", LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg",
LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg", LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg",
LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg", LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg",
LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg", LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg",
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg", LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg",
}; LLMProviders.IONOS => $"{ICON_ROOT}/ionos.svg",
LLMProviders.LITE_LLM => $"{ICON_ROOT}/litellm.svg",
LLMProviders.REQUESTY => $"{ICON_ROOT}/requesty.svg",
LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg",
LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg",
LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg",
LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg",
LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg",
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
};
}
private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty; private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty;
} }
@@ -6,16 +6,19 @@ public static class ModelLoadFailureReasonExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelLoadFailureReasonExtensions).Namespace, nameof(ModelLoadFailureReasonExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ModelLoadFailureReasonExtensions).Namespace, nameof(ModelLoadFailureReasonExtensions));
public static string ToUserMessage(this ModelLoadFailureReason failureReason, string providerName) => failureReason switch extension(ModelLoadFailureReason failureReason)
{ {
ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName), public string ToUserMessage(string providerName) => failureReason switch
ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName), {
ModelLoadFailureReason.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName), ModelLoadFailureReason.INVALID_OR_MISSING_API_KEY => string.Format(TB("We could not load models from '{0}'. The API key is probably missing, invalid, or expired."), providerName),
ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), providerName), ModelLoadFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => string.Format(TB("We could not load models from '{0}'. The account or API key does not have the required permissions."), providerName),
ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName), ModelLoadFailureReason.INSUFFICIENT_QUOTA => string.Format(TB("We could not load models from '{0}' because the account appears to have no API credits left."), providerName),
ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName), ModelLoadFailureReason.TOO_MANY_REQUESTS => string.Format(TB("We could not load models from '{0}' because too many requests were sent. Please wait a moment and try again."), providerName),
ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName), ModelLoadFailureReason.PROVIDER_UNAVAILABLE => string.Format(TB("We could not load models from '{0}' because the provider is currently unavailable or could not be reached."), providerName),
ModelLoadFailureReason.INVALID_RESPONSE => string.Format(TB("We could not load models from '{0}' because the provider returned an unexpected response."), providerName),
ModelLoadFailureReason.UNKNOWN => string.Format(TB("We could not load models from '{0}' due to an unknown error."), providerName),
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -4,22 +4,25 @@ namespace AIStudio.Provider.OpenAI;
public static class SystemPromptRoleExtensions public static class SystemPromptRoleExtensions
{ {
/// <summary>
/// Spells a system prompt role the way OpenAI's APIs expect it.
/// </summary>
/// <remarks>
/// A role no rule states becomes "developer". OpenAI's current documentation names no other, a
/// model the rules do not know is almost always one released after they were written, and the
/// older generations which still want "system" say so in their families.
/// </remarks>
/// <param name="role">The role the model profile states.</param> /// <param name="role">The role the model profile states.</param>
/// <returns>The role, as OpenAI names it.</returns> extension(SystemPromptRole role)
public static string ToOpenAIRole(this SystemPromptRole role) => role switch
{ {
SystemPromptRole.SYSTEM => "system", /// <summary>
SystemPromptRole.DEVELOPER => "developer", /// Spells a system prompt role the way OpenAI's APIs expect it.
SystemPromptRole.USER => "user", /// </summary>
/// <remarks>
/// A role no rule states becomes "developer". OpenAI's current documentation names no other, a
/// model the rules do not know is almost always one released after they were written, and the
/// older generations which still want "system" say so in their families.
/// </remarks>
/// <returns>The role, as OpenAI names it.</returns>
public string ToOpenAIRole() => role switch
{
SystemPromptRole.SYSTEM => "system",
SystemPromptRole.DEVELOPER => "developer",
SystemPromptRole.USER => "user",
_ => "developer", _ => "developer",
}; };
}
} }
@@ -6,42 +6,45 @@ public static class ProviderRequestFailureReasonExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderRequestFailureReasonExtensions).Namespace, nameof(ProviderRequestFailureReasonExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderRequestFailureReasonExtensions).Namespace, nameof(ProviderRequestFailureReasonExtensions));
/// <summary> extension(ProviderRequestFailureReason failureReason)
/// Names the kind of failure in a few words.
/// </summary>
/// <remarks>
/// Meant as a label beside the full message, so a list of failures can be scanned instead of
/// read: twenty entries which all say API key are one problem, not twenty.
/// </remarks>
public static string GetName(this ProviderRequestFailureReason failureReason) => failureReason switch
{ {
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"), /// <summary>
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"), /// Names the kind of failure in a few words.
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"), /// </summary>
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"), /// <remarks>
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"), /// Meant as a label beside the full message, so a list of failures can be scanned instead of
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"), /// read: twenty entries which all say API key are one problem, not twenty.
ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"), /// </remarks>
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"), public string GetName() => failureReason switch
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"), {
ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"), ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"),
ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"), ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"),
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER => TB("Model not offered"),
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY => TB("API key problem"),
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR => TB("Not permitted"),
ProviderRequestFailureReason.PROVIDER_UNAVAILABLE => TB("Provider unreachable"),
ProviderRequestFailureReason.MODEL_NOT_FOUND => TB("Model unknown"),
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED => TB("Text too long"),
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED => TB("No embeddings"),
ProviderRequestFailureReason.INVALID_RESPONSE => TB("Unreadable answer"),
ProviderRequestFailureReason.UNKNOWN => TB("Unknown cause"),
_ => string.Empty, _ => string.Empty,
}; };
/// <summary> /// <summary>
/// Gets a value indicating whether the way out of this failure is in the provider settings. /// Gets a value indicating whether the way out of this failure is in the provider settings.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Only for the failures a setting actually fixes. Pointing at the settings for a provider /// Only for the failures a setting actually fixes. Pointing at the settings for a provider
/// which is merely overloaded would send the user looking for a mistake they never made. /// which is merely overloaded would send the user looking for a mistake they never made.
/// </remarks> /// </remarks>
public static bool IsFixedInProviderSettings(this ProviderRequestFailureReason failureReason) => failureReason is public bool IsFixedInProviderSettings() => failureReason is
ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or ProviderRequestFailureReason.INVALID_OR_MISSING_API_KEY or
ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or ProviderRequestFailureReason.AUTHENTICATION_OR_PERMISSION_ERROR or
ProviderRequestFailureReason.MODEL_NOT_FOUND or ProviderRequestFailureReason.MODEL_NOT_FOUND or
ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or ProviderRequestFailureReason.MODEL_NOT_SUPPORTED_BY_PROVIDER or
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED; ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED;
}
} }
@@ -2,141 +2,143 @@ namespace AIStudio.Provider.SelfHosted;
public static class HostExtensions public static class HostExtensions
{ {
public static string Name(this Host host) => host switch
{
Host.NONE => "None",
Host.LM_STUDIO => "LM Studio",
Host.LLAMA_CPP => "llama.cpp",
Host.WHISPER_CPP => "whisper.cpp",
Host.OLLAMA => "ollama",
Host.VLLM => "vLLM",
Host.LLMMAN => "llmman",
_ => "Unknown",
};
public static string BaseURL(this Host host) => host switch
{
_ => "/v1/",
};
public static string ChatURL(this Host host) => host switch
{
_ => "chat/completions",
};
public static string TranscriptionURL(this Host host) => host switch
{
_ => "audio/transcriptions",
};
public static string EmbeddingURL(this Host host) => host switch
{
_ => "embeddings",
};
/// <summary>
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
/// </summary>
/// <remarks>
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <param name="host">The engine.</param> /// <param name="host">The engine.</param>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns> extension(Host host)
public static long? GetMaxImageSizeBytes(this Host host) => host switch
{ {
// public string Name() => host switch
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
// keeps the default limit of its web framework, read on 2026-10-22 at
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
//
Host.LLMMAN => 1_048_576,
_ => null,
};
public static bool IsChatSupported(this Host host)
{
switch (host)
{ {
case Host.WHISPER_CPP: Host.NONE => "None",
return false;
default: Host.LM_STUDIO => "LM Studio",
case Host.OLLAMA: Host.LLAMA_CPP => "llama.cpp",
case Host.VLLM: Host.WHISPER_CPP => "whisper.cpp",
case Host.LM_STUDIO: Host.OLLAMA => "ollama",
case Host.LLAMA_CPP: Host.VLLM => "vLLM",
case Host.LLMMAN: Host.LLMMAN => "llmman",
return true;
_ => "Unknown",
};
public string BaseURL() => host switch
{
_ => "/v1/",
};
public string ChatURL() => host switch
{
_ => "chat/completions",
};
public string TranscriptionURL() => host switch
{
_ => "audio/transcriptions",
};
public string EmbeddingURL() => host switch
{
_ => "embeddings",
};
/// <summary>
/// The largest image file, in bytes, which the engine accepts in a chat request, where we know of one.
/// </summary>
/// <remarks>
/// Like the limit of a cloud provider, see LLMProvidersExtensions.GetMaxImageSizeBytes, this
/// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks>
/// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public long? GetMaxImageSizeBytes() => host switch
{
//
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
// keeps the default limit of its web framework, read on 2026-10-22 at
// https://github.com/llmmanorg/llmman/blob/main/src/cmd/serve.rs. An image of 1 MiB takes
// about 1.33 MiB as base64, which leaves room for the text of the conversation:
//
Host.LLMMAN => 1_048_576,
_ => null,
};
public bool IsChatSupported()
{
switch (host)
{
case Host.WHISPER_CPP:
return false;
default:
case Host.OLLAMA:
case Host.VLLM:
case Host.LM_STUDIO:
case Host.LLAMA_CPP:
case Host.LLMMAN:
return true;
}
} }
}
public static bool IsEmbeddingSupported(this Host host) public bool IsEmbeddingSupported()
{
switch (host)
{ {
case Host.LM_STUDIO: switch (host)
case Host.OLLAMA: {
case Host.VLLM: case Host.LM_STUDIO:
case Host.LLMMAN: case Host.OLLAMA:
return true; case Host.VLLM:
case Host.LLMMAN:
return true;
default: default:
case Host.LLAMA_CPP: case Host.LLAMA_CPP:
return false; return false;
}
} }
}
public static bool IsTranscriptionSupported(this Host host) public bool IsTranscriptionSupported()
{
switch (host)
{ {
case Host.OLLAMA: switch (host)
case Host.VLLM: {
case Host.WHISPER_CPP: case Host.OLLAMA:
case Host.LLMMAN: case Host.VLLM:
return true; case Host.WHISPER_CPP:
case Host.LLMMAN:
return true;
default: default:
case Host.LM_STUDIO: case Host.LM_STUDIO:
case Host.LLAMA_CPP: case Host.LLAMA_CPP:
return false; return false;
}
} }
}
/// <summary> /// <summary>
/// Whether the engine answers the models route with the models it serves. /// Whether the engine answers the models route with the models it serves.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// This is the only place which says so. The provider asks it before it loads the models of a /// This is the only place which says so. The provider asks it before it loads the models of a
/// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload /// chat, embedding, or transcription list, and the dialogs ask it before they offer to reload
/// them; a new engine therefore needs no more than its line here. whisper.cpp is not among /// them; a new engine therefore needs no more than its line here. whisper.cpp is not among
/// them: it serves the one model it was started with and has no route to list it, so the /// them: it serves the one model it was started with and has no route to list it, so the
/// provider offers that model under a fixed name instead. llama.cpp is, even though builds /// provider offers that model under a fixed name instead. llama.cpp is, even though builds
/// older than the route answer it with 404; the provider handles those builds itself. /// older than the route answer it with 404; the provider handles those builds itself.
/// </remarks> /// </remarks>
/// <param name="host">The engine.</param> /// <returns>True when the models can be loaded from the engine; otherwise, false.</returns>
/// <returns>True when the models can be loaded from the engine; otherwise, false.</returns> public bool IsModelListSupported()
public static bool IsModelListSupported(this Host host)
{
switch (host)
{ {
case Host.LLAMA_CPP: switch (host)
case Host.OLLAMA: {
case Host.LM_STUDIO: case Host.LLAMA_CPP:
case Host.VLLM: case Host.OLLAMA:
case Host.LLMMAN: case Host.LM_STUDIO:
return true; case Host.VLLM:
case Host.LLMMAN:
return true;
default: default:
case Host.NONE: case Host.NONE:
case Host.WHISPER_CPP: case Host.WHISPER_CPP:
return false; return false;
}
} }
} }
} }
@@ -2,54 +2,57 @@ namespace AIStudio.Settings.DataModel;
public static class BiasCategoryExtensions public static class BiasCategoryExtensions
{ {
public static string ToName(this BiasCategory biasCategory) => biasCategory switch extension(BiasCategory biasCategory)
{ {
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?", public string ToName() => biasCategory switch
BiasCategory.TOO_MUCH_INFORMATION => "Too much information", {
BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning", BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.NEED_TO_ACT_FAST => "Need to act fast", BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
BiasCategory.NOT_ENOUGH_MEANING => "Not enough meaning",
BiasCategory.NEED_TO_ACT_FAST => "Need to act fast",
_ => "Unknown category", _ => "Unknown category",
}; };
public static string GetThoughts(this BiasCategory biasCategory) => biasCategory switch public string GetThoughts() => biasCategory switch
{ {
BiasCategory.WHAT_SHOULD_WE_REMEMBER => BiasCategory.WHAT_SHOULD_WE_REMEMBER =>
""" """
- We store memories differently based on how they were experienced - We store memories differently based on how they were experienced
- We reduce events and lists to their key elements - We reduce events and lists to their key elements
- We discard specifics to form generalities - We discard specifics to form generalities
- We edit and reinforce some memories after the fact - We edit and reinforce some memories after the fact
""", """,
BiasCategory.TOO_MUCH_INFORMATION => BiasCategory.TOO_MUCH_INFORMATION =>
""" """
- We notice things already primed in memory or repeated often - We notice things already primed in memory or repeated often
- Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things - Bizarre, funny, visually-striking, or anthropomorphic things stick out more than non-bizarre/unfunny things
- We notice when something has changed - We notice when something has changed
- We are drawn to details that confirm our own existing beliefs - We are drawn to details that confirm our own existing beliefs
- We notice flaws in others more easily than we notice flaws in ourselves - We notice flaws in others more easily than we notice flaws in ourselves
""", """,
BiasCategory.NOT_ENOUGH_MEANING => BiasCategory.NOT_ENOUGH_MEANING =>
""" """
- We tend to find stories and patterns even when looking at sparse data - We tend to find stories and patterns even when looking at sparse data
- We fill in characteristics from stereotypes, generalities, and prior histories - We fill in characteristics from stereotypes, generalities, and prior histories
- We imagine things and people we’re familiar with or fond of as better - We imagine things and people we’re familiar with or fond of as better
- We simplify probabilities and numbers to make them easier to think about - We simplify probabilities and numbers to make them easier to think about
- We project our current mindset and assumptions onto the past and future - We project our current mindset and assumptions onto the past and future
- We think we know what other people are thinking - We think we know what other people are thinking
""", """,
BiasCategory.NEED_TO_ACT_FAST => BiasCategory.NEED_TO_ACT_FAST =>
""" """
- We favor simple-looking options and complete information over complex, ambiguous options - We favor simple-looking options and complete information over complex, ambiguous options
- To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions - To avoid mistakes, we aim to preserve autonomy and group status, and avoid irreversible decisions
- To get things done, we tend to complete things we’ve invested time & energy in - To get things done, we tend to complete things we’ve invested time & energy in
- To stay focused, we favor the immediate, relatable thing in front of us - To stay focused, we favor the immediate, relatable thing in front of us
- To act, we must be confident we can make an impact and feel what we do is important - To act, we must be confident we can make an impact and feel what we do is important
""", """,
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -6,50 +6,56 @@ public static class DataSourceSecurityExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceSecurityExtensions).Namespace, nameof(DataSourceSecurityExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceSecurityExtensions).Namespace, nameof(DataSourceSecurityExtensions));
public static string ToSelectionText(this DataSourceSecurity security) => security switch extension(DataSourceSecurity security)
{ {
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"), public string ToSelectionText() => security switch
{
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"),
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
_ => TB("Unknown security policy") _ => TB("Unknown security policy")
}; };
public static string ToInfoText(this DataSourceSecurity security) => security switch public string ToInfoText() => security switch
{ {
DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."), DataSourceSecurity.NOT_SPECIFIED => TB("The security of the data source is not specified yet. You cannot use this data source until you specify a security policy."),
DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."), DataSourceSecurity.ALLOW_ANY => TB("This data source can be used with any LLM provider. Your data may be sent to a cloud-based provider."),
DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."), DataSourceSecurity.SELF_HOSTED => TB("This data source can only be used with a self-hosted LLM provider. Your data will not be sent to any cloud-based provider."),
_ => TB("Unknown security policy") _ => TB("Unknown security policy")
}; };
public TextColor GetColor() => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
_ => TextColor.ERROR
};
}
/// <summary>
/// The data security which demands what the one held and the one arriving demand together.
/// </summary>
/// <remarks>
/// Data which may only be used with self-hosted providers keeps them as the only choice, no matter
/// what arrives later: the data was seen. Data which may be used with any provider marks the holder
/// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED
/// demands nothing and changes nothing. The order does not matter.
/// </remarks>
/// <param name="held">The data security held so far.</param> /// <param name="held">The data security held so far.</param>
/// <param name="arriving">What the data arriving demands.</param> extension(DataSourceSecurity held)
/// <returns>The tightened data security; never looser than either of the two.</returns>
public static DataSourceSecurity TightenedBy(this DataSourceSecurity held, DataSourceSecurity arriving) => (held, arriving) switch
{ {
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED, /// <summary>
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY, /// The data security which demands what the one held and the one arriving demand together.
_ => held, /// </summary>
}; /// <remarks>
/// Data which may only be used with self-hosted providers keeps them as the only choice, no matter
public static TextColor GetColor(this DataSourceSecurity security) => security switch /// what arrives later: the data was seen. Data which may be used with any provider marks the holder
{ /// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED
DataSourceSecurity.ALLOW_ANY => TextColor.WARN, /// demands nothing and changes nothing. The order does not matter.
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS, /// </remarks>
/// <param name="arriving">What the data arriving demands.</param>
_ => TextColor.ERROR /// <returns>The tightened data security; never looser than either of the two.</returns>
}; public DataSourceSecurity TightenedBy(DataSourceSecurity arriving) => (held, arriving) switch
{
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED,
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY,
_ => held,
};
}
} }
@@ -9,18 +9,21 @@ public static class DataSourceTypeExtension
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceTypeExtension).Namespace, nameof(DataSourceTypeExtension)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(DataSourceTypeExtension).Namespace, nameof(DataSourceTypeExtension));
/// <summary>
/// Get the display name of the data source type.
/// </summary>
/// <param name="type">The data source type.</param> /// <param name="type">The data source type.</param>
/// <returns>The display name of the data source type.</returns> extension(DataSourceType type)
public static string GetDisplayName(this DataSourceType type) => type switch
{ {
DataSourceType.LOCAL_FILE => TB("Local File"), /// <summary>
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"), /// Get the display name of the data source type.
DataSourceType.ERI_V1 => TB("External ERI Server (v1)"), /// </summary>
DataSourceType.MAILBOX => TB("Mailbox"), /// <returns>The display name of the data source type.</returns>
public string GetDisplayName() => type switch
{
DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
DataSourceType.ERI_V1 => TB("External ERI Server (v1)"),
DataSourceType.MAILBOX => TB("Mailbox"),
_ => TB("None"), _ => TB("None"),
}; };
}
} }
@@ -6,11 +6,14 @@ public static class LangBehaviorExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LangBehaviorExtensions).Namespace, nameof(LangBehaviorExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LangBehaviorExtensions).Namespace, nameof(LangBehaviorExtensions));
public static string Name(this LangBehavior langBehavior) => langBehavior switch extension(LangBehavior langBehavior)
{ {
LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."), public string Name() => langBehavior switch
LangBehavior.MANUAL => TB("Choose the language manually."), {
LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."),
LangBehavior.MANUAL => TB("Choose the language manually."),
_ => TB("Unknown option") _ => TB("Unknown option")
}; };
}
} }
@@ -6,31 +6,34 @@ public static class MailboxMaxAgeExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxMaxAgeExtensions).Namespace, nameof(MailboxMaxAgeExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxMaxAgeExtensions).Namespace, nameof(MailboxMaxAgeExtensions));
public static string GetName(this MailboxMaxAge maxAge) => maxAge switch
{
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"),
MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"),
MailboxMaxAge.ALL => TB("All mails"),
_ => TB("Unknown period"),
};
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="maxAge">The period.</param> /// <param name="maxAge">The period.</param>
/// <param name="now">The current time.</param> extension(MailboxMaxAge maxAge)
/// <returns>The first day, or null when the period has no end.</returns>
public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch
{ {
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6), public string GetName() => maxAge switch
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12), {
MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24), MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.ALL => null, MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
MailboxMaxAge.LAST_12_MONTHS => TB("The last 12 months"),
MailboxMaxAge.LAST_24_MONTHS => TB("The last 24 months"),
MailboxMaxAge.ALL => TB("All mails"),
// The shortest period also stands in for a value this version does not know, as it does in the settings: _ => TB("Unknown period"),
_ => now.AddMonths(-3), };
};
/// <summary>
/// The first day of the period, as the sync asks the server for it.
/// </summary>
/// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns>
public DateTimeOffset? GetReceivedSince(DateTimeOffset now) => maxAge switch
{
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
MailboxMaxAge.LAST_24_MONTHS => now.AddMonths(-24),
MailboxMaxAge.ALL => null,
// The shortest period also stands in for a value this version does not know, as it does in the settings:
_ => now.AddMonths(-3),
};
}
} }
@@ -16,23 +16,26 @@ public static class MailboxTransportSecurityExtensions
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxTransportSecurityExtensions).Namespace, nameof(MailboxTransportSecurityExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxTransportSecurityExtensions).Namespace, nameof(MailboxTransportSecurityExtensions));
public static string GetName(this MailboxTransportSecurity transportSecurity) => transportSecurity switch extension(MailboxTransportSecurity transportSecurity)
{ {
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"), public string GetName() => transportSecurity switch
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"), {
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
_ => TB("Unknown encryption"), _ => TB("Unknown encryption"),
}; };
/// <summary> /// <summary>
/// The port the encryption usually comes with. /// The port the encryption usually comes with.
/// </summary> /// </summary>
/// <returns>The port, or null when there is no usual one.</returns> /// <returns>The port, or null when there is no usual one.</returns>
public static int? GetUsualPort(this MailboxTransportSecurity transportSecurity) => transportSecurity switch public int? GetUsualPort() => transportSecurity switch
{ {
MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT, MailboxTransportSecurity.SSL_ON_CONNECT => SSL_ON_CONNECT_PORT,
MailboxTransportSecurity.STARTTLS => STARTTLS_PORT, MailboxTransportSecurity.STARTTLS => STARTTLS_PORT,
_ => null, _ => null,
}; };
}
} }
@@ -6,31 +6,34 @@ public static class OutboundDataRequirementExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRequirementExtensions).Namespace, nameof(OutboundDataRequirementExtensions));
/// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary>
/// <remarks>
/// One text for the tool selection and for a call which was turned down, so the user reads the
/// same in both places, and the model can pass it on. It names the source which demands the
/// requirement, and the setting as the user finds it labeled. The level is the one the setting
/// had when the content was read: a change made later does not loosen the chat.<br/><br/>
/// The text speaks of content the chat holds, not of content it read: the content may come from
/// another chat or an assistant, which sent it here along with its restrictions.
/// </remarks>
/// <param name="requirement">What the chat demands.</param> /// <param name="requirement">What the chat demands.</param>
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param> extension(OutboundDataRequirement requirement)
/// <returns>The text for the user.</returns>
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch
{ {
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes), /// <summary>
OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), /// Why a tool cannot run in a chat with this requirement, and how to get it back.
OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()), /// </summary>
OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources), /// <remarks>
/// One text for the tool selection and for a call which was turned down, so the user reads the
/// same in both places, and the model can pass it on. It names the source which demands the
/// requirement, and the setting as the user finds it labeled. The level is the one the setting
/// had when the content was read: a change made later does not loosen the chat.<br/><br/>
/// The text speaks of content the chat holds, not of content it read: the content may come from
/// another chat or an assistant, which sent it here along with its restrictions.
/// </remarks>
/// <param name="settings">The configuration, to find the name of the mailbox or data source which demands it.</param>
/// <returns>The text for the user.</returns>
public string GetToolBlockedMessage(Data settings) => requirement.Origin switch
{
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes),
OutboundDataOrigin.CONFLUENCE_WIKI => string.Format(TB("This chat holds content from your Confluence wiki, found while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
OutboundDataOrigin.INTERNAL_WEB_PAGE => string.Format(TB("This chat holds content from an internal web page on '{0}', read while the setting 'Where a chat may send data after reading internal pages' of the tool 'Read Web Page' was '{1}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Reference, requirement.Restriction.GetName()),
OutboundDataOrigin.DATA_SOURCE => GetDataSourceBlockedMessage(requirement, settings.DataSources),
// NONE keeps no tool from running, since nothing restricts such a chat. What is left is an // NONE keeps no tool from running, since nothing restricts such a chat. What is left is an
// origin this version does not know: // origin this version does not know:
_ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()), _ => string.Format(TB("This chat holds internal content, read while a chat was restricted to '{0}'. This tool would send data beyond that, so it is not available in this chat. A new chat without this content can use it again."), requirement.Restriction.GetName()),
}; };
}
private static string GetMailboxBlockedMessage(OutboundDataRequirement requirement, IEnumerable<DataSourceMailbox> mailboxes) private static string GetMailboxBlockedMessage(OutboundDataRequirement requirement, IEnumerable<DataSourceMailbox> mailboxes)
{ {
@@ -6,52 +6,54 @@ public static class OutboundDataRestrictionExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(OutboundDataRestrictionExtensions).Namespace, nameof(OutboundDataRestrictionExtensions));
/// <summary> /// <param name="restriction">The restriction, as it was read.</param>
/// The stricter of two restrictions. extension(OutboundDataRestriction restriction)
/// </summary>
/// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks>
/// <param name="restriction">The one restriction.</param>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other)
{ {
var knownRestriction = restriction.OrStrictestWhenUnknown(); /// <summary>
var knownOther = other.OrStrictestWhenUnknown(); /// The stricter of two restrictions.
return knownOther < knownRestriction ? knownOther : knownRestriction; /// </summary>
/// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks>
/// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns>
public OutboundDataRestriction StricterOf(OutboundDataRestriction other)
{
var knownRestriction = restriction.OrStrictestWhenUnknown();
var knownOther = other.OrStrictestWhenUnknown();
return knownOther < knownRestriction ? knownOther : knownRestriction;
}
/// <summary>
/// The restriction as this version applies it.
/// </summary>
/// <remarks>
/// The settings file and a saved chat may hold a number which names no member, e.g., one written
/// by hand, and the settings read it all the same. Compared as it is, such a number would rank
/// above UNRESTRICTED and lift every restriction it meets.
/// </remarks>
/// <returns>The restriction itself, or the strictest one when this version does not know it.</returns>
public OutboundDataRestriction OrStrictestWhenUnknown() => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public string GetName() => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
OutboundDataRestriction.UNRESTRICTED => TB("No restriction"),
_ => TB("Unknown restriction"),
};
/// <summary>
/// What a chat may still do on this level, once internal content entered it.
/// </summary>
public string GetDescription() => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."),
OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."),
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
} }
/// <summary>
/// The restriction as this version applies it.
/// </summary>
/// <remarks>
/// The settings file and a saved chat may hold a number which names no member, e.g., one written
/// by hand, and the settings read it all the same. Compared as it is, such a number would rank
/// above UNRESTRICTED and lift every restriction it meets.
/// </remarks>
/// <param name="restriction">The restriction as it was read.</param>
/// <returns>The restriction itself, or the strictest one when this version does not know it.</returns>
public static OutboundDataRestriction OrStrictestWhenUnknown(this OutboundDataRestriction restriction) => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public static string GetName(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
OutboundDataRestriction.UNRESTRICTED => TB("No restriction"),
_ => TB("Unknown restriction"),
};
/// <summary>
/// What a chat may still do on this level, once internal content entered it.
/// </summary>
public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch
{
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("The chat may only use services configured in AI Studio: your mailboxes, your data sources, your Confluence wiki, and the allowed private hosts of the tool 'Read Web Page'. It reads no other web pages, and it does not search the web."),
OutboundDataRestriction.ONLY_LINKS_FROM_CHAT => TB("The chat may also read web pages whose addresses stand in the chat, written by you or returned by a tool, exactly as they stand there. It does not search the web, and the AI cannot choose addresses of its own outside the services configured in AI Studio."),
OutboundDataRestriction.UNRESTRICTED => TB("The chat may use every tool you chose, web search and any web page included. Internal content, such as your mails or the documents of your data sources, can then reach third parties, e.g., inside a search query or the address of a web page."),
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
};
} }
@@ -6,46 +6,49 @@ public static class PreviewFeaturesExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PreviewFeaturesExtensions).Namespace, nameof(PreviewFeaturesExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(PreviewFeaturesExtensions).Namespace, nameof(PreviewFeaturesExtensions));
public static string GetPreviewDescription(this PreviewFeatures feature) => feature switch
{
PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"),
PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"),
PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"),
_ => TB("Unknown preview feature")
};
/// <summary>
/// Check if the preview feature is released or not.
/// </summary>
/// <remarks>
/// This metadata is necessary because we cannot remove the enum values from the list.
/// Otherwise, we could not deserialize old settings files that may contain these values.
/// </remarks>
/// <param name="feature">The preview feature to check.</param> /// <param name="feature">The preview feature to check.</param>
/// <returns>True when the preview feature is released, false otherwise.</returns> extension(PreviewFeatures feature)
public static bool IsReleased(this PreviewFeatures feature) => feature switch
{ {
PreviewFeatures.PRE_READ_PDF_2025 => true, public string GetPreviewDescription() => feature switch
PreviewFeatures.PRE_PLUGINS_2025 => true, {
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true, PreviewFeatures.PRE_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"),
_ => false PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"),
}; PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"),
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"),
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"),
PreviewFeatures.PRE_META_ASSISTANT_V1 => TB("Assistant Builder: Generate and install assistant plugins"),
PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026 => TB("Visual Briefing Assistant: Turn source material into an interactive briefing"),
PreviewFeatures.PRE_MAILBOXES_2026 => TB("Mailboxes: Let the AI search and read your e-mails, kept in a local index on your computer"),
public static bool IsEnabled(this PreviewFeatures feature, SettingsManager settingsManager) _ => TB("Unknown preview feature")
{ };
if(feature.IsReleased())
return true;
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature); /// <summary>
/// Check if the preview feature is released or not.
/// </summary>
/// <remarks>
/// This metadata is necessary because we cannot remove the enum values from the list.
/// Otherwise, we could not deserialize old settings files that may contain these values.
/// </remarks>
/// <returns>True when the preview feature is released, false otherwise.</returns>
public bool IsReleased() => feature switch
{
PreviewFeatures.PRE_READ_PDF_2025 => true,
PreviewFeatures.PRE_PLUGINS_2025 => true,
PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true,
PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true,
_ => false
};
public bool IsEnabled(SettingsManager settingsManager)
{
if(feature.IsReleased())
return true;
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature);
}
} }
} }
@@ -2,48 +2,51 @@ namespace AIStudio.Settings.DataModel;
public static class PreviewVisibilityExtensions public static class PreviewVisibilityExtensions
{ {
public static IList<PreviewFeatures> GetPreviewFeatures(this PreviewVisibility visibility) extension(PreviewVisibility visibility)
{ {
var features = new List<PreviewFeatures>(); public IList<PreviewFeatures> GetPreviewFeatures()
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
{ {
var features = new List<PreviewFeatures>();
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
{
}
if (visibility >= PreviewVisibility.BETA)
{
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025);
features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1);
features.Add(PreviewFeatures.PRE_RAG_2024);
features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026);
}
if (visibility >= PreviewVisibility.ALPHA)
{
}
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_MAILBOXES_2026);
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
}
return features;
} }
if (visibility >= PreviewVisibility.BETA) public HashSet<PreviewFeatures> FilterPreviewFeatures(HashSet<PreviewFeatures> enabledFeatures)
{ {
features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025); var filteredFeatures = new HashSet<PreviewFeatures>();
features.Add(PreviewFeatures.PRE_META_ASSISTANT_V1); var previewFeatures = visibility.GetPreviewFeatures();
features.Add(PreviewFeatures.PRE_RAG_2024); foreach (var feature in enabledFeatures)
features.Add(PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026); {
if (previewFeatures.Contains(feature))
filteredFeatures.Add(feature);
}
return filteredFeatures;
} }
if (visibility >= PreviewVisibility.ALPHA)
{
}
if (visibility >= PreviewVisibility.PROTOTYPE)
{
features.Add(PreviewFeatures.PRE_MAILBOXES_2026);
}
if (visibility >= PreviewVisibility.EXPERIMENTAL)
{
features.Add(PreviewFeatures.PRE_WRITER_MODE_2024);
}
return features;
}
public static HashSet<PreviewFeatures> FilterPreviewFeatures(this PreviewVisibility visibility, HashSet<PreviewFeatures> enabledFeatures)
{
var filteredFeatures = new HashSet<PreviewFeatures>();
var previewFeatures = visibility.GetPreviewFeatures();
foreach (var feature in enabledFeatures)
{
if (previewFeatures.Contains(feature))
filteredFeatures.Add(feature);
}
return filteredFeatures;
} }
} }
@@ -2,20 +2,26 @@ namespace AIStudio.Settings.DataModel.PreviousModels;
public static class ProviderV4Extensions public static class ProviderV4Extensions
{ {
public static List<AIStudio.Settings.Provider> MigrateFromV4ToV5(this IEnumerable<Provider> providers) extension(IEnumerable<Provider> providers)
{ {
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList(); public List<AIStudio.Settings.Provider> MigrateFromV4ToV5()
{
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList();
}
} }
public static AIStudio.Settings.Provider MigrateFromV4ToV5(this Provider provider) => new() extension(Provider provider)
{ {
Num = provider.Num, public AIStudio.Settings.Provider MigrateFromV4ToV5() => new()
Id = provider.Id, {
InstanceName = provider.InstanceName, Num = provider.Num,
UsedLLMProvider = provider.UsedProvider, Id = provider.Id,
Model = provider.Model, InstanceName = provider.InstanceName,
IsSelfHosted = provider.IsSelfHosted, UsedLLMProvider = provider.UsedProvider,
Hostname = provider.Hostname, Model = provider.Model,
Host = provider.Host, IsSelfHosted = provider.IsSelfHosted,
}; Hostname = provider.Hostname,
Host = provider.Host,
};
}
} }
@@ -6,11 +6,14 @@ public static class SendToChatDataSourceBehaviorExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SendToChatDataSourceBehaviorExtensions).Namespace, nameof(SendToChatDataSourceBehaviorExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(SendToChatDataSourceBehaviorExtensions).Namespace, nameof(SendToChatDataSourceBehaviorExtensions));
public static string Description(this SendToChatDataSourceBehavior behavior) => behavior switch extension(SendToChatDataSourceBehavior behavior)
{ {
SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"), public string Description() => behavior switch
SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"), {
SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, when sending an assistant result to a chat"),
SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"),
_ => TB("Unknown behavior"), _ => TB("Unknown behavior"),
}; };
}
} }
@@ -2,16 +2,19 @@ namespace AIStudio.Settings.DataModel;
public static class StartPageExtensions public static class StartPageExtensions
{ {
public static string ToRoute(this StartPage startPage) => startPage switch extension(StartPage startPage)
{ {
StartPage.HOME => string.Empty, public string ToRoute() => startPage switch
StartPage.CHAT => Routes.CHAT, {
StartPage.ASSISTANTS => Routes.ASSISTANTS, StartPage.HOME => string.Empty,
StartPage.INFORMATION => Routes.ABOUT, StartPage.CHAT => Routes.CHAT,
StartPage.PLUGINS => Routes.PLUGINS, StartPage.ASSISTANTS => Routes.ASSISTANTS,
StartPage.SUPPORTERS => Routes.SUPPORTERS, StartPage.INFORMATION => Routes.ABOUT,
StartPage.SETTINGS => Routes.SETTINGS, StartPage.PLUGINS => Routes.PLUGINS,
StartPage.SUPPORTERS => Routes.SUPPORTERS,
StartPage.SETTINGS => Routes.SETTINGS,
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -6,12 +6,15 @@ public static class ThemesExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ThemesExtensions).Namespace, nameof(ThemesExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ThemesExtensions).Namespace, nameof(ThemesExtensions));
public static string GetName(this Themes theme) => theme switch extension(Themes theme)
{ {
Themes.SYSTEM => TB("Synchronized with the operating system settings"), public string GetName() => theme switch
Themes.LIGHT => TB("Always use light theme"), {
Themes.DARK => TB("Always use dark theme"), Themes.SYSTEM => TB("Synchronized with the operating system settings"),
Themes.LIGHT => TB("Always use light theme"),
Themes.DARK => TB("Always use dark theme"),
_ => TB("Unknown setting"), _ => TB("Unknown setting"),
}; };
}
} }
@@ -6,24 +6,27 @@ public static class TranscriptionOpusBitrateExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(TranscriptionOpusBitrateExtensions).Namespace, nameof(TranscriptionOpusBitrateExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(TranscriptionOpusBitrateExtensions).Namespace, nameof(TranscriptionOpusBitrateExtensions));
public static string GetName(this TranscriptionOpusBitrate bitrate) => bitrate switch extension(TranscriptionOpusBitrate bitrate)
{ {
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"), public string GetName() => bitrate switch
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"), {
TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"), TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"), TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
_ => TB("Unknown"), TranscriptionOpusBitrate.KBPS_128 => TB("128 kbps (recommended)"),
}; TranscriptionOpusBitrate.KBPS_256 => TB("256 kbps (largest upload, highest accuracy)"),
_ => TB("Unknown"),
};
public static uint GetBitsPerSecond(this TranscriptionOpusBitrate bitrate) => bitrate switch public uint GetBitsPerSecond() => bitrate switch
{ {
TranscriptionOpusBitrate.KBPS_32 => 32_000, TranscriptionOpusBitrate.KBPS_32 => 32_000,
TranscriptionOpusBitrate.KBPS_64 => 64_000, TranscriptionOpusBitrate.KBPS_64 => 64_000,
TranscriptionOpusBitrate.KBPS_128 => 128_000, TranscriptionOpusBitrate.KBPS_128 => 128_000,
TranscriptionOpusBitrate.KBPS_256 => 256_000, TranscriptionOpusBitrate.KBPS_256 => 256_000,
// A value we do not know must never mean the lowest quality: that is how quiet passages // A value we do not know must never mean the lowest quality: that is how quiet passages
// went missing from transcripts in the first place. Fall back to the recommended bitrate. // went missing from transcripts in the first place. Fall back to the recommended bitrate.
_ => 128_000, _ => 128_000,
}; };
}
} }
@@ -2,20 +2,23 @@ namespace AIStudio.Settings;
public static class DataSourceOrderExtensions public static class DataSourceOrderExtensions
{ {
/// <summary>
/// Sorts data sources in a fixed order: by their number, then by their ID.
/// </summary>
/// <remarks>
/// The same data sources always come in the same order, however the settings list them, the
/// checks return them, or an agent selects them. Semantic Search offers them this way, because
/// the providers cache a request from its beginning, and the tools are part of that beginning,
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks>
/// <param name="dataSources">The data sources to sort.</param> /// <param name="dataSources">The data sources to sort.</param>
/// <returns>The data sources in their fixed order.</returns> extension(IEnumerable<IDataSource> dataSources)
public static IReadOnlyList<IDataSource> InFixedOrder(this IEnumerable<IDataSource> dataSources) => dataSources {
.OrderBy(dataSource => dataSource.Num) /// <summary>
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal) /// Sorts data sources in a fixed order: by their number, then by their ID.
.ToList(); /// </summary>
/// <remarks>
/// The same data sources always come in the same order, however the settings list them, the
/// checks return them, or an agent selects them. Semantic Search offers them this way, because
/// the providers cache a request from its beginning, and the tools are part of that beginning,
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks>
/// <returns>The data sources in their fixed order.</returns>
public IReadOnlyList<IDataSource> InFixedOrder() => dataSources
.OrderBy(dataSource => dataSource.Num)
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal)
.ToList();
}
} }
@@ -30,26 +30,28 @@ public static class DataSourceOutboundDataExtensions
? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES) ? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)
: organizationMinimum; : organizationMinimum;
/// <summary>
/// The least strict level this data source may have.
/// </summary>
/// <param name="dataSource">The data source.</param> /// <param name="dataSource">The data source.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param> extension(IDataSource dataSource)
/// <returns>The least strict level allowed.</returns> {
public static OutboundDataRestriction GetMinimumOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => /// <summary>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum); /// The least strict level this data source may have.
/// </summary>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <returns>The least strict level allowed.</returns>
public OutboundDataRestriction GetMinimumOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum);
/// <summary> /// <summary>
/// Where a chat may still send data, once passages of this data source reached it. /// Where a chat may still send data, once passages of this data source reached it.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// The level of the data source, tightened to the least strict one it may have. This holds at /// The level of the data source, tightened to the least strict one it may have. This holds at
/// runtime, not only in the dialog: a data source stored before, or one a configuration plugin /// runtime, not only in the dialog: a data source stored before, or one a configuration plugin
/// provides, is never edited, so its stored level may be less strict. /// provides, is never edited, so its stored level may be less strict.
/// </remarks> /// </remarks>
/// <param name="dataSource">The data source.</param> /// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param>
/// <param name="organizationMinimum">The least strict level the organization allows, see DataSourceSecuritySettings.MinimumOutboundDataRestriction.</param> /// <returns>The level which applies.</returns>
/// <returns>The level which applies.</returns> public OutboundDataRestriction GetEffectiveOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
public static OutboundDataRestriction GetEffectiveOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum));
dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum)); }
} }
@@ -5,160 +5,184 @@ namespace AIStudio.Settings;
public static class DataSourceSecurityTrustExtensions public static class DataSourceSecurityTrustExtensions
{ {
public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager) extension(Provider provider)
{ {
if (provider == Provider.NONE) public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager settingsManager)
{
if (provider is NoProvider)
return false;
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
}
public static ConfidenceLevel GetConfidenceLevel(this Provider provider, SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this EmbeddingProvider provider, SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this TranscriptionProvider provider, SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public static ConfidenceLevel GetConfidenceLevel(this IProvider provider, SettingsManager settingsManager)
{
if (provider is NoProvider)
return ConfidenceLevel.NONE;
return provider.Provider.GetConfidence(settingsManager).Level;
}
public static bool AllowsDataSourceAccess(this Provider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public static bool AllowsDataSourceAccess(this IProvider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public static bool AllowsDataSourceSecurity(this Provider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public static bool AllowsDataSourceSecurity(this IProvider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public static bool AllowsDataSourceSecurity(this bool usingTrustedProvider, DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
{
DataSourceSecurity.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false,
};
public static bool AllowsDataSourceConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel requiredConfidenceLevel)
{
if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true;
return providerConfidenceLevel >= requiredConfidenceLevel;
}
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// Only the levels from very low to high qualify. NONE lets every provider through, cf.
/// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or
/// below the level of a provider nobody rated, so even such a provider would meet them.
/// </remarks>
public static bool IsAllowedMailboxConfidence(this ConfidenceLevel confidenceLevel) => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
/// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary>
/// <remarks>
/// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every
/// provider, whether it was never chosen or edited by hand in the settings file. Every provider
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails.
/// </remarks>
public static bool AllowsMailboxConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel mailboxConfidenceLevel)
{
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false;
return providerConfidenceLevel >= mailboxConfidenceLevel;
}
public static ConfidenceLevel GetRequiredConfidenceLevel(this IEnumerable<IDataSource> dataSources)
{
var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
if (dataSource.ConfidenceLevel > requiredConfidenceLevel)
requiredConfidenceLevel = dataSource.ConfidenceLevel;
return requiredConfidenceLevel;
}
public static DataSourceSecurity GetRequiredSecurityPolicy(this IEnumerable<IDataSource> dataSources)
{
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
{ {
if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED) if (provider == Provider.NONE)
return DataSourceSecurity.NOT_SPECIFIED; return false;
if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED) return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED;
} }
return requiredSecurityPolicy; public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == Provider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
} }
public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); extension(EmbeddingProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return false;
public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager); public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == EmbeddingProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
extension(TranscriptionProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return false;
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
}
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider == TranscriptionProvider.NONE)
return ConfidenceLevel.NONE;
return provider.UsedLLMProvider.GetConfidence(settingsManager).Level;
}
public bool IsTrustedByConfiguration(SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
}
extension(IProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{
if (provider is NoProvider)
return false;
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
}
public ConfidenceLevel GetConfidenceLevel(SettingsManager settingsManager)
{
if (provider is NoProvider)
return ConfidenceLevel.NONE;
return provider.Provider.GetConfidence(settingsManager).Level;
}
public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
}
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
}
extension(bool usingTrustedProvider)
{
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
{
DataSourceSecurity.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false,
};
}
extension(ConfidenceLevel providerConfidenceLevel)
{
public bool AllowsDataSourceConfidenceLevel(ConfidenceLevel requiredConfidenceLevel)
{
if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true;
return providerConfidenceLevel >= requiredConfidenceLevel;
}
/// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary>
/// <remarks>
/// Unlike AllowsDataSourceConfidenceLevel, this closes a mailbox without an allowed level to every
/// provider, whether it was never chosen or edited by hand in the settings file. Every provider
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails.
/// </remarks>
public bool AllowsMailboxConfidenceLevel(ConfidenceLevel mailboxConfidenceLevel)
{
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false;
return providerConfidenceLevel >= mailboxConfidenceLevel;
}
}
extension(ConfidenceLevel confidenceLevel)
{
/// <summary>
/// Whether a mailbox may require this provider confidence level.
/// </summary>
/// <remarks>
/// Only the levels from very low to high qualify. NONE lets every provider through, cf.
/// AllowsDataSourceConfidenceLevel, and UNTRUSTED and UNKNOWN do almost the same: both lie at or
/// below the level of a provider nobody rated, so even such a provider would meet them.
/// </remarks>
public bool IsAllowedMailboxConfidence() => confidenceLevel is ConfidenceLevel.VERY_LOW or ConfidenceLevel.LOW or ConfidenceLevel.MODERATE or ConfidenceLevel.MEDIUM or ConfidenceLevel.HIGH;
}
extension(IEnumerable<IDataSource> dataSources)
{
public ConfidenceLevel GetRequiredConfidenceLevel()
{
var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
if (dataSource.ConfidenceLevel > requiredConfidenceLevel)
requiredConfidenceLevel = dataSource.ConfidenceLevel;
return requiredConfidenceLevel;
}
public DataSourceSecurity GetRequiredSecurityPolicy()
{
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
{
if (dataSource.SecurityPolicy is DataSourceSecurity.NOT_SPECIFIED)
return DataSourceSecurity.NOT_SPECIFIED;
if (dataSource.SecurityPolicy is DataSourceSecurity.SELF_HOSTED)
requiredSecurityPolicy = DataSourceSecurity.SELF_HOSTED;
}
return requiredSecurityPolicy;
}
}
private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager) private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager)
{ {
@@ -6,38 +6,41 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions public static partial class ProviderExtensions
{ {
/// <summary>
/// Get the effective reasoning indicator state for the configured provider instance.
/// </summary>
/// <remarks>
/// Two answers meet here, and they answer different questions. What a model is able to do comes
/// from the rules; what this person asked for comes from the parameters they wrote into their
/// own provider. A model which thinks unless told otherwise stops showing the indicator when a
/// parameter turns it off, and a model which can be asked to think shows it only once one does.
/// </remarks>
/// <param name="provider">The configured provider.</param> /// <param name="provider">The configured provider.</param>
/// <returns>The effective reasoning indicator state.</returns> extension(Provider provider)
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider)
{ {
var reasoning = provider.GetModelProfile().Reasoning; /// <summary>
if (reasoning is ReasoningSupport.ALWAYS) /// Get the effective reasoning indicator state for the configured provider instance.
return ReasoningIndicatorState.ALWAYS_ON; /// </summary>
/// <remarks>
var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters); /// Two answers meet here, and they answer different questions. What a model is able to do comes
if (reasoning is ReasoningSupport.ON_BY_DEFAULT) /// from the rules; what this person asked for comes from the parameters they wrote into their
/// own provider. A model which thinks unless told otherwise stops showing the indicator when a
/// parameter turns it off, and a model which can be asked to think shows it only once one does.
/// </remarks>
/// <returns>The effective reasoning indicator state.</returns>
public ReasoningIndicatorState GetReasoningIndicatorState()
{ {
return configured switch var reasoning = provider.GetModelProfile().Reasoning;
if (reasoning is ReasoningSupport.ALWAYS)
return ReasoningIndicatorState.ALWAYS_ON;
var configured = ReasoningDispatcher.WhatTheParametersSay(provider.UsedLLMProvider, provider.Host, provider.AdditionalJsonApiParameters);
if (reasoning is ReasoningSupport.ON_BY_DEFAULT)
{ {
ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE, return configured switch
ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED, {
ReasoningConfigurationState.EXPLICITLY_DISABLED => ReasoningIndicatorState.NONE,
ReasoningConfigurationState.EXPLICITLY_ENABLED => ReasoningIndicatorState.CONFIGURED,
_ => ReasoningIndicatorState.DEFAULT_ON, _ => ReasoningIndicatorState.DEFAULT_ON,
}; };
}
if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED)
return ReasoningIndicatorState.CONFIGURED;
return ReasoningIndicatorState.NONE;
} }
if (reasoning is ReasoningSupport.OPTIONAL && configured is ReasoningConfigurationState.EXPLICITLY_ENABLED)
return ReasoningIndicatorState.CONFIGURED;
return ReasoningIndicatorState.NONE;
} }
} }
@@ -7,132 +7,136 @@ namespace AIStudio.Settings;
public static partial class ProviderExtensions public static partial class ProviderExtensions
{ {
/// <summary>
/// Everything the app knows about the model this provider instance is configured with.
/// </summary>
/// <remarks>
/// The one door to that question. Behind it stand the links of the chain, in the order they
/// win: what the person said about their own installation, then what the installation itself
/// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything.
/// </remarks>
/// <param name="provider">The configured provider.</param> /// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model.</returns> extension(Provider provider)
public static ModelProfile GetModelProfile(this Provider provider)
{ {
var automatic = provider.GetAutomaticModelProfile(); /// <summary>
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic; /// Everything the app knows about the model this provider instance is configured with.
/// </summary>
/// <remarks>
/// The one door to that question. Behind it stand the links of the chain, in the order they
/// win: what the person said about their own installation, then what the installation itself
/// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything.
/// </remarks>
/// <returns>The profile of the configured model.</returns>
public ModelProfile GetModelProfile()
{
var automatic = provider.GetAutomaticModelProfile();
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
}
/// <summary>
/// Everything known about the configured model except what the person themselves switched.
/// </summary>
/// <remarks>
/// This is what happens when somebody fills in nothing, which is why the expert dialog shows it
/// as the automatic answer. It has to include what the provider reported: a person who leaves
/// the window empty gets the number their own engine stated, and a placeholder showing them a
/// different one would be a promise the app does not keep.
/// </remarks>
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
public ModelProfile GetAutomaticModelProfile()
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
}
/// <summary>
/// Get whether the model used by the configured provider accepts images as input.
/// </summary>
/// <remarks>
/// Two capabilities express image input, one for a single image and one for several. Anything that
/// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it.
///
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator
/// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks>
/// <returns><c>true</c> when the model accepts image input.</returns>
public bool SupportsImageInput()
{
var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
}
} }
/// <summary>
/// Everything known about the configured model except what the person themselves switched.
/// </summary>
/// <remarks>
/// This is what happens when somebody fills in nothing, which is why the expert dialog shows it
/// as the automatic answer. It has to include what the provider reported: a person who leaves
/// the window empty gets the number their own engine stated, and a placeholder showing them a
/// different one would be a promise the app does not keep.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model, without that provider's overrides.</returns>
public static ModelProfile GetAutomaticModelProfile(this Provider provider)
{
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated);
}
/// <summary>
/// Everything the rules know about a model at a provider, without anybody's own installation.
/// </summary>
/// <remarks>
/// The answer to the model as such, which is the same for everybody who uses that name at that
/// provider -- and therefore the answer the registry caches. What one particular installation
/// says about it is asked one link further up, where the instance is known.
///
/// The assumed profile fills in where no rule stated a single capability. It fills in the
/// capabilities only: a modifier may well have said what the model is made for without any rule
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
/// model rather than turning into a chat model with an assumption attached.
/// </remarks>
/// <param name="provider">The LLM provider the model is reached through.</param> /// <param name="provider">The LLM provider the model is reached through.</param>
/// <param name="model">The model, named the way that provider names it.</param> extension(LLMProviders provider)
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public static ModelProfile GetModelProfile(this LLMProviders provider, Model model)
{ {
// /// <summary>
// Without a provider there is nothing to reach the model through, and an empty name is what /// Everything the rules know about a model at a provider, without anybody's own installation.
// a provider reports before anybody picked one. Neither is a model we could assume anything /// </summary>
// about, so neither gets the assumption. /// <remarks>
// /// The answer to the model as such, which is the same for everybody who uses that name at that
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id)) /// provider -- and therefore the answer the registry caches. What one particular installation
return ModelProfile.UNKNOWN; /// says about it is asked one link further up, where the instance is known.
///
/// The assumed profile fills in where no rule stated a single capability. It fills in the
/// capabilities only: a modifier may well have said what the model is made for without any rule
/// saying what it can do, and an embedding model nobody wrote a rule for stays an embedding
/// model rather than turning into a chat model with an assumption attached.
/// </remarks>
/// <param name="model">The model, named the way that provider names it.</param>
/// <returns>The profile, which knows nothing when there is nothing to reach.</returns>
public ModelProfile GetModelProfile(Model model)
{
//
// Without a provider there is nothing to reach the model through, and an empty name is what
// a provider reports before anybody picked one. Neither is a model we could assume anything
// about, so neither gets the assumption.
//
if (provider is LLMProviders.NONE || string.IsNullOrWhiteSpace(model.Id))
return ModelProfile.UNKNOWN;
var stated = ModelRegistry.Shared.Profile(provider, model.Id); var stated = ModelRegistry.Shared.Profile(provider, model.Id);
return stated.Capabilities is Capability.NONE return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities } ? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated; : stated;
}
} }
/// <summary> /// <param name="model">The model to check.</param>
/// Get whether the model used by the configured provider accepts images as input. extension(Model model)
/// </summary>
/// <remarks>
/// Two capabilities express image input, one for a single image and one for several. Anything that
/// wants to know whether an image may be sent has to accept both, which is why the question is asked
/// here instead of at each call site: attaching a file and validating an already attached file must
/// never disagree about it.
///
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator
/// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider)
{ {
var profile = provider.GetModelProfile(); /// <summary>
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0; /// Checks whether this model can be used for chatting.
/// </summary>
/// <remarks>
/// What a model can do and what it is made for used to be two questions answered by two pieces
/// of code, each walking the same name with rules of its own. They disagreed: a model like
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
/// come out of the same rules now, which is why this takes the provider -- the same name means
/// different things depending on who serves it, and only the provider knows how to unwrap it.
///
/// The direction of the answer is deliberate. Everything not recognized as something else is a
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
/// paying for it. Getting it wrong the other way would hide a model.
/// </remarks>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
public bool IsChatModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary>
/// Checks whether this model creates embeddings.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns>
public bool IsEmbeddingModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary>
/// Checks whether this model transcribes audio.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns>
public bool IsTranscriptionModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary>
/// Checks whether this model generates images.
/// </summary>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns>
public bool IsImageModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
} }
/// <summary>
/// Checks whether this model can be used for chatting.
/// </summary>
/// <remarks>
/// What a model can do and what it is made for used to be two questions answered by two pieces
/// of code, each walking the same name with rules of its own. They disagreed: a model like
/// nomic-embed-text was an embedding model at one provider and a chat model at the next. Both
/// come out of the same rules now, which is why this takes the provider -- the same name means
/// different things depending on who serves it, and only the provider knows how to unwrap it.
///
/// The direction of the answer is deliberate. Everything not recognized as something else is a
/// chat model, so a provider adding a family we have never seen keeps it visible to the person
/// paying for it. Getting it wrong the other way would hide a model.
/// </remarks>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a chat model or when we recognize no other kind.</returns>
public static bool IsChatModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary>
/// Checks whether this model creates embeddings.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns>
public static bool IsEmbeddingModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary>
/// Checks whether this model transcribes audio.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns>
public static bool IsTranscriptionModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary>
/// Checks whether this model generates images.
/// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns>
public static bool IsImageModel(this Model model, LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
} }
@@ -6,26 +6,29 @@ public static class AssistantCategoryExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantCategoryExtensions).Namespace, nameof(AssistantCategoryExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantCategoryExtensions).Namespace, nameof(AssistantCategoryExtensions));
public static string Name(this AssistantCategory category) => category switch extension(AssistantCategory category)
{ {
AssistantCategory.AS_IS => TB("Select the assistant category"), public string Name() => category switch
AssistantCategory.GENERAL => TB("General"), {
AssistantCategory.SCIENTIFIC => TB("Scientific"), AssistantCategory.AS_IS => TB("Select the assistant category"),
AssistantCategory.BUSINESS => TB("Business"), AssistantCategory.GENERAL => TB("General"),
AssistantCategory.PRODUCTIVITY => TB("Productivity"), AssistantCategory.SCIENTIFIC => TB("Scientific"),
AssistantCategory.DEVELOPMENT => TB("Software Development"), AssistantCategory.BUSINESS => TB("Business"),
AssistantCategory.LEARNING => TB("Learning"), AssistantCategory.PRODUCTIVITY => TB("Productivity"),
AssistantCategory.AI_STUDIO => TB("AI Studio Development"), AssistantCategory.DEVELOPMENT => TB("Software Development"),
AssistantCategory.OTHER => TB("Other"), AssistantCategory.LEARNING => TB("Learning"),
AssistantCategory.AI_STUDIO => TB("AI Studio Development"),
AssistantCategory.OTHER => TB("Other"),
_ => string.Empty, _ => string.Empty,
}; };
public static string NameSelecting(this AssistantCategory category) public string NameSelecting()
{ {
if(category is AssistantCategory.AS_IS) if(category is AssistantCategory.AS_IS)
return TB("Please select the assistant category"); return TB("Please select the assistant category");
return category.Name(); return category.Name();
}
} }
} }
@@ -10,86 +10,89 @@ public static class AssistantVisibilityExtensions
{ {
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantVisibilityExtensions)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(AssistantVisibilityExtensions));
/// <summary>
/// Checks if an assistant should be visible based on configuration and optional preview feature requirements.
/// </summary>
/// <param name="settingsManager">The settings manager to check configuration against.</param> /// <param name="settingsManager">The settings manager to check configuration against.</param>
/// <param name="withLogging">Whether to log visibility decisions.</param> extension(SettingsManager settingsManager)
/// <param name="assistantName">The name of the assistant to check (for logging purposes).</param>
/// <param name="component">The assistant component to check.</param>
/// <param name="requiredPreviewFeature">Optional preview feature that must be enabled for the assistant to be visible.</param>
/// <returns>True if the assistant should be visible, false otherwise.</returns>
public static bool IsAssistantVisible(this SettingsManager settingsManager, Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
{ {
withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName); /// <summary>
/// Checks if an assistant should be visible based on configuration and optional preview feature requirements.
// Check if a preview feature is required and enabled: /// </summary>
if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager)) /// <param name="withLogging">Whether to log visibility decisions.</param>
/// <param name="assistantName">The name of the assistant to check (for logging purposes).</param>
/// <param name="component">The assistant component to check.</param>
/// <param name="requiredPreviewFeature">Optional preview feature that must be enabled for the assistant to be visible.</param>
/// <returns>True if the assistant should be visible, false otherwise.</returns>
public bool IsAssistantVisible(Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
{ {
if(withLogging) withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName);
LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature);
return false; // Check if a preview feature is required and enabled:
if (requiredPreviewFeature != PreviewFeatures.NONE && !requiredPreviewFeature.IsEnabled(settingsManager))
{
if(withLogging)
LOGGER.LogInformation("Assistant '{AssistantName}' is not visible because the required preview feature '{PreviewFeature}' is not enabled.", assistantName, requiredPreviewFeature);
return false;
}
// If no component is specified, it's always visible:
if (component is Components.NONE)
{
if(withLogging)
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName);
return true;
}
// Dynamic assistants are controlled through plugin activation and their security state.
// The Assistant Builder is controlled through its preview feature. Neither belongs to the
// built-in assistant visibility list, so both are expected to be visible at this point.
if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT)
return true;
// Map Components enum to ConfigurableAssistant enum:
var configurableAssistant = component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT,
Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT,
Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT,
Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT,
Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT,
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT,
Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT,
Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT,
Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT,
Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT,
Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT,
Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT,
Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT,
Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT,
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT,
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT,
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
_ => ConfigurableAssistant.UNKNOWN,
};
// If the component doesn't map to a configurable assistant, it's always visible:
if (configurableAssistant is ConfigurableAssistant.UNKNOWN)
{
if(withLogging)
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component);
return true;
}
// Check if the assistant is hidden by any configuration plugin:
var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant);
if (isHidden && withLogging)
LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName);
return !isHidden;
} }
// If no component is specified, it's always visible:
if (component is Components.NONE)
{
if(withLogging)
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because no component is specified.", assistantName);
return true;
}
// Dynamic assistants are controlled through plugin activation and their security state.
// The Assistant Builder is controlled through its preview feature. Neither belongs to the
// built-in assistant visibility list, so both are expected to be visible at this point.
if (component is Components.DYNAMIC_ASSISTANT or Components.META_ASSISTANT)
return true;
// Map Components enum to ConfigurableAssistant enum:
var configurableAssistant = component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => ConfigurableAssistant.GRAMMAR_SPELLING_ASSISTANT,
Components.ICON_FINDER_ASSISTANT => ConfigurableAssistant.ICON_FINDER_ASSISTANT,
Components.REWRITE_ASSISTANT => ConfigurableAssistant.REWRITE_ASSISTANT,
Components.PROMPT_OPTIMIZER_ASSISTANT => ConfigurableAssistant.PROMPT_OPTIMIZER_ASSISTANT,
Components.TRANSLATION_ASSISTANT => ConfigurableAssistant.TRANSLATION_ASSISTANT,
Components.AGENDA_ASSISTANT => ConfigurableAssistant.AGENDA_ASSISTANT,
Components.CODING_ASSISTANT => ConfigurableAssistant.CODING_ASSISTANT,
Components.TEXT_SUMMARIZER_ASSISTANT => ConfigurableAssistant.TEXT_SUMMARIZER_ASSISTANT,
Components.SWOT_ANALYSIS_ASSISTANT => ConfigurableAssistant.SWOT_ANALYSIS_ASSISTANT,
Components.EMAIL_ASSISTANT => ConfigurableAssistant.EMAIL_ASSISTANT,
Components.LEGAL_CHECK_ASSISTANT => ConfigurableAssistant.LEGAL_CHECK_ASSISTANT,
Components.SYNONYMS_ASSISTANT => ConfigurableAssistant.SYNONYMS_ASSISTANT,
Components.MY_TASKS_ASSISTANT => ConfigurableAssistant.MY_TASKS_ASSISTANT,
Components.JOB_POSTING_ASSISTANT => ConfigurableAssistant.JOB_POSTING_ASSISTANT,
Components.BIAS_DAY_ASSISTANT => ConfigurableAssistant.BIAS_DAY_ASSISTANT,
Components.ERI_ASSISTANT => ConfigurableAssistant.ERI_ASSISTANT,
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfigurableAssistant.DOCUMENT_ANALYSIS_ASSISTANT,
Components.BATCH_PROCESSING_ASSISTANT => ConfigurableAssistant.BATCH_PROCESSING_ASSISTANT,
Components.SLIDE_BUILDER_ASSISTANT => ConfigurableAssistant.SLIDE_BUILDER_ASSISTANT,
Components.VISUAL_BRIEFING_ASSISTANT => ConfigurableAssistant.VISUAL_BRIEFING_ASSISTANT,
Components.I18N_ASSISTANT => ConfigurableAssistant.I18N_ASSISTANT,
Components.LOG_VIEWER_ASSISTANT => ConfigurableAssistant.LOG_VIEWER_ASSISTANT,
_ => ConfigurableAssistant.UNKNOWN,
};
// If the component doesn't map to a configurable assistant, it's always visible:
if (configurableAssistant is ConfigurableAssistant.UNKNOWN)
{
if(withLogging)
LOGGER.LogWarning("Assistant '{AssistantName}' is visible because its component '{Component}' does not map to a configurable assistant.", assistantName, component);
return true;
}
// Check if the assistant is hidden by any configuration plugin:
var isHidden = settingsManager.ConfigurationData.App.HiddenAssistants.Contains(configurableAssistant);
if (isHidden && withLogging)
LOGGER.LogInformation("Assistant '{AssistantName}' is hidden based on the configuration.", assistantName);
return !isHidden;
} }
} }
@@ -7,13 +7,16 @@ public static class AuthMethodsV1Extensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AuthMethodsV1Extensions).Namespace, nameof(AuthMethodsV1Extensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AuthMethodsV1Extensions).Namespace, nameof(AuthMethodsV1Extensions));
public static string DisplayName(this AuthMethod authMethod) => authMethod switch extension(AuthMethod authMethod)
{ {
AuthMethod.NONE => TB("None"), public string DisplayName() => authMethod switch
AuthMethod.USERNAME_PASSWORD => TB("Username & Password"), {
AuthMethod.KERBEROS => TB("SSO (Kerberos)"), AuthMethod.NONE => TB("None"),
AuthMethod.TOKEN => TB("Access Token"), AuthMethod.USERNAME_PASSWORD => TB("Username & Password"),
AuthMethod.KERBEROS => TB("SSO (Kerberos)"),
AuthMethod.TOKEN => TB("Access Token"),
_ => TB("Unknown authentication method"), _ => TB("Unknown authentication method"),
}; };
}
} }
@@ -4,10 +4,13 @@ namespace AIStudio.Tools;
public static class ChatTemplateExtensions public static class ChatTemplateExtensions
{ {
public static IEnumerable<ChatTemplate> GetAllChatTemplates(this IEnumerable<ChatTemplate> chatTemplates) extension(IEnumerable<ChatTemplate> chatTemplates)
{ {
yield return ChatTemplate.NO_CHAT_TEMPLATE; public IEnumerable<ChatTemplate> GetAllChatTemplates()
foreach (var chatTemplate in chatTemplates) {
yield return chatTemplate; yield return ChatTemplate.NO_CHAT_TEMPLATE;
foreach (var chatTemplate in chatTemplates)
yield return chatTemplate;
}
} }
} }
@@ -2,26 +2,29 @@ namespace AIStudio.Tools.CodeHighlighting;
public static class CodeTokenKindExtensions public static class CodeTokenKindExtensions
{ {
/// <summary>
/// The CSS class which MudBlazor.Markdown gives this kind of part, so that it takes its color from
/// the palette of the theme, just as the languages the library highlights itself.
/// </summary>
/// <param name="kind">What the part is.</param> /// <param name="kind">What the part is.</param>
/// <returns>The CSS class of the part.</returns> extension(CodeTokenKind kind)
public static string ToCssClass(this CodeTokenKind kind) => kind switch
{ {
CodeTokenKind.KEYWORD => "hljs-keyword", /// <summary>
CodeTokenKind.TYPE => "hljs-type", /// The CSS class which MudBlazor.Markdown gives this kind of part, so that it takes its color from
CodeTokenKind.LITERAL => "hljs-literal", /// the palette of the theme, just as the languages the library highlights itself.
CodeTokenKind.STRING => "hljs-string", /// </summary>
CodeTokenKind.COMMENT => "hljs-comment", /// <returns>The CSS class of the part.</returns>
CodeTokenKind.NUMBER => "hljs-number", public string ToCssClass() => kind switch
CodeTokenKind.FUNCTION => "hljs-title function_", {
CodeTokenKind.META => "hljs-meta", CodeTokenKind.KEYWORD => "hljs-keyword",
CodeTokenKind.VARIABLE => "hljs-variable", CodeTokenKind.TYPE => "hljs-type",
CodeTokenKind.ATTRIBUTE => "hljs-attr", CodeTokenKind.LITERAL => "hljs-literal",
CodeTokenKind.SELECTOR => "hljs-selector-class", CodeTokenKind.STRING => "hljs-string",
CodeTokenKind.COMMENT => "hljs-comment",
CodeTokenKind.NUMBER => "hljs-number",
CodeTokenKind.FUNCTION => "hljs-title function_",
CodeTokenKind.META => "hljs-meta",
CodeTokenKind.VARIABLE => "hljs-variable",
CodeTokenKind.ATTRIBUTE => "hljs-attr",
CodeTokenKind.SELECTOR => "hljs-selector-class",
_ => string.Empty, _ => string.Empty,
}; };
}
} }
@@ -6,80 +6,83 @@ public static class CommonLanguageExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(CommonLanguageExtensions).Namespace, nameof(CommonLanguageExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(CommonLanguageExtensions).Namespace, nameof(CommonLanguageExtensions));
public static string Name(this CommonLanguages language) => language switch extension(CommonLanguages language)
{ {
CommonLanguages.AS_IS => TB("Do not change the language"), public string Name() => language switch
{
CommonLanguages.AS_IS => TB("Do not change the language"),
CommonLanguages.EN_US => TB("English (US)"), CommonLanguages.EN_US => TB("English (US)"),
CommonLanguages.EN_GB => TB("English (UK)"), CommonLanguages.EN_GB => TB("English (UK)"),
CommonLanguages.ZH_CN => TB("Chinese (Simplified)"), CommonLanguages.ZH_CN => TB("Chinese (Simplified)"),
CommonLanguages.HI_IN => TB("Hindi (India)"), CommonLanguages.HI_IN => TB("Hindi (India)"),
CommonLanguages.ES_ES => TB("Spanish (Spain)"), CommonLanguages.ES_ES => TB("Spanish (Spain)"),
CommonLanguages.FR_FR => TB("French (France)"), CommonLanguages.FR_FR => TB("French (France)"),
CommonLanguages.DE_DE => TB("German (Germany)"), CommonLanguages.DE_DE => TB("German (Germany)"),
CommonLanguages.DE_AT => TB("German (Austria)"), CommonLanguages.DE_AT => TB("German (Austria)"),
CommonLanguages.DE_CH => TB("German (Switzerland)"), CommonLanguages.DE_CH => TB("German (Switzerland)"),
CommonLanguages.JA_JP => TB("Japanese (Japan)"), CommonLanguages.JA_JP => TB("Japanese (Japan)"),
CommonLanguages.RU_RU => TB("Russian (Russia)"), CommonLanguages.RU_RU => TB("Russian (Russia)"),
_ => TB("Other"), _ => TB("Other"),
}; };
public static string ToIETFTag(this CommonLanguages language) => language switch public string ToIETFTag() => language switch
{ {
CommonLanguages.AS_IS => string.Empty, CommonLanguages.AS_IS => string.Empty,
CommonLanguages.EN_US => "en-US", CommonLanguages.EN_US => "en-US",
CommonLanguages.EN_GB => "en-GB", CommonLanguages.EN_GB => "en-GB",
CommonLanguages.ZH_CN => "zh-CN", CommonLanguages.ZH_CN => "zh-CN",
CommonLanguages.HI_IN => "hi-IN", CommonLanguages.HI_IN => "hi-IN",
CommonLanguages.ES_ES => "es-ES", CommonLanguages.ES_ES => "es-ES",
CommonLanguages.FR_FR => "fr-FR", CommonLanguages.FR_FR => "fr-FR",
CommonLanguages.DE_DE => "de-DE", CommonLanguages.DE_DE => "de-DE",
CommonLanguages.DE_AT => "de-AT", CommonLanguages.DE_AT => "de-AT",
CommonLanguages.DE_CH => "de-CH", CommonLanguages.DE_CH => "de-CH",
CommonLanguages.JA_JP => "ja-JP", CommonLanguages.JA_JP => "ja-JP",
CommonLanguages.RU_RU => "ru-RU", CommonLanguages.RU_RU => "ru-RU",
_ => string.Empty, _ => string.Empty,
}; };
public static string PromptSummarizing(this CommonLanguages language, string customLanguage) => language switch public string PromptSummarizing(string customLanguage) => language switch
{ {
CommonLanguages.AS_IS => "Use the language the text is written in for the summary.", CommonLanguages.AS_IS => "Use the language the text is written in for the summary.",
CommonLanguages.OTHER => $"Output your summary in {customLanguage}.", CommonLanguages.OTHER => $"Output your summary in {customLanguage}.",
_ => $"Output your summary in {language.Name()} ({language}).", _ => $"Output your summary in {language.Name()} ({language}).",
}; };
public static string PromptTranslation(this CommonLanguages language, string customLanguage) => language switch public string PromptTranslation(string customLanguage) => language switch
{ {
CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.", CommonLanguages.OTHER => $"Translate the source text to {customLanguage}.",
_ => $"Translate the source text to {language.Name()} ({language}).", _ => $"Translate the source text to {language.Name()} ({language}).",
}; };
public static string PromptGeneralPurpose(this CommonLanguages language, string customLanguage) => language switch public string PromptGeneralPurpose(string customLanguage) => language switch
{ {
CommonLanguages.AS_IS => "Use the language the user input is written in for the output.", CommonLanguages.AS_IS => "Use the language the user input is written in for the output.",
CommonLanguages.OTHER => $"use the language {customLanguage} for your output.", CommonLanguages.OTHER => $"use the language {customLanguage} for your output.",
_ => $"Use the language {language.Name()} ({language}) for your output.", _ => $"Use the language {language.Name()} ({language}) for your output.",
}; };
public static string NameSelecting(this CommonLanguages language) public string NameSelecting()
{ {
if(language is CommonLanguages.AS_IS) if(language is CommonLanguages.AS_IS)
return TB("Please select the target language"); return TB("Please select the target language");
return language.Name(); return language.Name();
} }
public static string NameSelectingOptional(this CommonLanguages language) public string NameSelectingOptional()
{ {
if(language is CommonLanguages.AS_IS) if(language is CommonLanguages.AS_IS)
return TB("Do not specify the language"); return TB("Do not specify the language");
return language.Name(); return language.Name();
}
} }
} }
@@ -10,236 +10,237 @@ public static class ComponentsExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ComponentsExtensions).Namespace, nameof(ComponentsExtensions));
/// <summary>
/// Gets the preview feature a component belongs to. Components that are generally available
/// return <see cref="PreviewFeatures.NONE"/>. This is the single place that maps a component to
/// its preview feature, so visibility checks never need to special-case one assistant.
/// </summary>
/// <param name="component">The component to look up.</param> /// <param name="component">The component to look up.</param>
/// <returns>The required preview feature.</returns> extension(Components component)
public static PreviewFeatures RequiredPreviewFeature(this Components component) => component switch
{ {
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026, /// <summary>
/// Gets the preview feature a component belongs to. Components that are generally available
_ => PreviewFeatures.NONE, /// return <see cref="PreviewFeatures.NONE"/>. This is the single place that maps a component to
}; /// its preview feature, so visibility checks never need to special-case one assistant.
/// </summary>
/// <summary> /// <returns>The required preview feature.</returns>
/// Gets whether a component owns exactly one assistant session slot, so that a running session public PreviewFeatures RequiredPreviewFeature() => component switch
/// blocks starting another one and inactive sessions can be cleared as a group.
/// </summary>
/// <remarks>
/// Components return <c>false</c> for three different reasons. The chat has no assistant sessions
/// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys
/// its sessions per briefing, so it owns one slot per stored briefing rather than one per
/// component. All must be excluded from the single-slot checks, which is why this is a
/// capability and not a component comparison.
/// </remarks>
/// <param name="component">The component to look up.</param>
/// <returns><c>true</c> when the component owns exactly one session slot.</returns>
public static bool HasSingleSessionSlot(this Components component) => component switch
{
Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
Components.VISUAL_BRIEFING_ASSISTANT => false,
_ => true,
};
/// <summary>
/// Gets the kind of media-import owner a component creates for its attachments.
/// </summary>
/// <param name="component">The component to look up.</param>
/// <returns>The media-import owner kind.</returns>
public static MediaImportOwnerKind MediaOwnerKind(this Components component) => component switch
{
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
_ => MediaImportOwnerKind.ASSISTANT,
};
public static bool AllowSendTo(this Components component) => component switch
{
Components.NONE => false,
Components.DYNAMIC_ASSISTANT => false,
Components.ERI_ASSISTANT => false,
Components.BIAS_DAY_ASSISTANT => false,
Components.I18N_ASSISTANT => false,
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
Components.BATCH_PROCESSING_ASSISTANT => false,
Components.LOG_VIEWER_ASSISTANT => false,
Components.APP_SETTINGS => false,
Components.WRITER => false,
Components.AGENT_TEXT_CONTENT_CLEANER => false,
Components.AGENT_DATA_SOURCE_SELECTION => false,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false,
Components.META_ASSISTANT => false,
_ => true,
};
public static string Name(this Components component) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"),
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"),
Components.AGENDA_ASSISTANT => TB("Agenda Assistant"),
Components.CODING_ASSISTANT => TB("Coding Assistant"),
Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"),
Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"),
Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"),
Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"),
Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"),
Components.ERI_ASSISTANT => TB("ERI Server"),
Components.I18N_ASSISTANT => TB("Localization Assistant"),
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"),
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"),
Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"),
Components.META_ASSISTANT => TB("Assistant Builder"),
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
Components.CHAT => TB("New Chat"),
_ => Enum.GetName(component)!,
};
public static ComponentsData GetData(this Components destination) => destination switch
{
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER),
Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS),
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS),
Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER),
Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING),
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
_ => new(Event.NONE, Routes.ASSISTANTS),
};
public static ConfidenceLevel MinimumConfidence(this Components component, SettingsManager settingsManager) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default,
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
// The minimum confidence for the Document Analysis Assistant is set per policy.
// We do this inside the Document Analysis Assistant component:
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE,
// A policy-specific minimum is merged with this component default inside
// the Batch Processing Assistant; the stricter level wins.
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default,
_ => default,
};
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
// The Document Analysis Assistant does not have a preselected provider at the component level.
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
// 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.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
_ => Settings.Provider.NONE,
};
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager)
{
var storedValue = component switch
{ {
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.PreselectedProfile : string.Empty, Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
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,
// 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,
// The Document Analysis Assistant does not have a preselected profile at the component level. _ => PreviewFeatures.NONE,
// The profile is selected per policy instead. We do this inside the Document Analysis Assistant component:
Components.DOCUMENT_ANALYSIS_ASSISTANT => Profile.NO_PROFILE.Id,
_ => string.Empty,
}; };
return ProfilePreselection.FromStoredValue(storedValue); /// <summary>
/// Gets whether a component owns exactly one assistant session slot, so that a running session
/// blocks starting another one and inactive sessions can be cleared as a group.
/// </summary>
/// <remarks>
/// Components return <c>false</c> for three different reasons. The chat has no assistant sessions
/// at all. Dynamic assistants key their sessions per plugin. The visual briefing assistant keys
/// its sessions per briefing, so it owns one slot per stored briefing rather than one per
/// component. All must be excluded from the single-slot checks, which is why this is a
/// capability and not a component comparison.
/// </remarks>
/// <returns><c>true</c> when the component owns exactly one session slot.</returns>
public bool HasSingleSessionSlot() => component switch
{
Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
Components.VISUAL_BRIEFING_ASSISTANT => false,
_ => true,
};
/// <summary>
/// Gets the kind of media-import owner a component creates for its attachments.
/// </summary>
/// <returns>The media-import owner kind.</returns>
public MediaImportOwnerKind MediaOwnerKind() => component switch
{
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
_ => MediaImportOwnerKind.ASSISTANT,
};
public bool AllowSendTo() => component switch
{
Components.NONE => false,
Components.DYNAMIC_ASSISTANT => false,
Components.ERI_ASSISTANT => false,
Components.BIAS_DAY_ASSISTANT => false,
Components.I18N_ASSISTANT => false,
Components.DOCUMENT_ANALYSIS_ASSISTANT => false,
Components.BATCH_PROCESSING_ASSISTANT => false,
Components.LOG_VIEWER_ASSISTANT => false,
Components.APP_SETTINGS => false,
Components.WRITER => false,
Components.AGENT_TEXT_CONTENT_CLEANER => false,
Components.AGENT_DATA_SOURCE_SELECTION => false,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => false,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => false,
Components.META_ASSISTANT => false,
_ => true,
};
public string Name() => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
Components.SWOT_ANALYSIS_ASSISTANT => TB("SWOT Analysis Assistant"),
Components.ICON_FINDER_ASSISTANT => TB("Icon Finder Assistant"),
Components.TRANSLATION_ASSISTANT => TB("Translation Assistant"),
Components.REWRITE_ASSISTANT => TB("Rewrite Assistant"),
Components.PROMPT_OPTIMIZER_ASSISTANT => TB("Prompt Optimizer Assistant"),
Components.AGENDA_ASSISTANT => TB("Agenda Assistant"),
Components.CODING_ASSISTANT => TB("Coding Assistant"),
Components.EMAIL_ASSISTANT => TB("E-Mail Assistant"),
Components.LEGAL_CHECK_ASSISTANT => TB("Legal Check Assistant"),
Components.SYNONYMS_ASSISTANT => TB("Synonym Assistant"),
Components.MY_TASKS_ASSISTANT => TB("My Tasks Assistant"),
Components.JOB_POSTING_ASSISTANT => TB("Job Posting Assistant"),
Components.ERI_ASSISTANT => TB("ERI Server"),
Components.I18N_ASSISTANT => TB("Localization Assistant"),
Components.DOCUMENT_ANALYSIS_ASSISTANT => TB("Document Analysis Assistant"),
Components.BATCH_PROCESSING_ASSISTANT => TB("Batch Processing Assistant"),
Components.SLIDE_BUILDER_ASSISTANT => TB("Slide Assistant"),
Components.VISUAL_BRIEFING_ASSISTANT => TB("Visual Briefing Assistant"),
Components.META_ASSISTANT => TB("Assistant Builder"),
Components.LOG_VIEWER_ASSISTANT => TB("Log Viewer Assistant"),
Components.CHAT => TB("New Chat"),
_ => Enum.GetName(component)!,
};
public ComponentsData GetData() => component switch
{
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA),
Components.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
Components.REWRITE_ASSISTANT => new(Event.SEND_TO_REWRITE_ASSISTANT, Routes.ASSISTANT_REWRITE),
Components.PROMPT_OPTIMIZER_ASSISTANT => new(Event.SEND_TO_PROMPT_OPTIMIZER_ASSISTANT, Routes.ASSISTANT_PROMPT_OPTIMIZER),
Components.EMAIL_ASSISTANT => new(Event.SEND_TO_EMAIL_ASSISTANT, Routes.ASSISTANT_EMAIL),
Components.TRANSLATION_ASSISTANT => new(Event.SEND_TO_TRANSLATION_ASSISTANT, Routes.ASSISTANT_TRANSLATION),
Components.ICON_FINDER_ASSISTANT => new(Event.SEND_TO_ICON_FINDER_ASSISTANT, Routes.ASSISTANT_ICON_FINDER),
Components.GRAMMAR_SPELLING_ASSISTANT => new(Event.SEND_TO_GRAMMAR_SPELLING_ASSISTANT, Routes.ASSISTANT_GRAMMAR_SPELLING),
Components.TEXT_SUMMARIZER_ASSISTANT => new(Event.SEND_TO_TEXT_SUMMARIZER_ASSISTANT, Routes.ASSISTANT_SUMMARIZER),
Components.SWOT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_SWOT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_SWOT_ANALYSIS),
Components.LEGAL_CHECK_ASSISTANT => new(Event.SEND_TO_LEGAL_CHECK_ASSISTANT, Routes.ASSISTANT_LEGAL_CHECK),
Components.SYNONYMS_ASSISTANT => new(Event.SEND_TO_SYNONYMS_ASSISTANT, Routes.ASSISTANT_SYNONYMS),
Components.MY_TASKS_ASSISTANT => new(Event.SEND_TO_MY_TASKS_ASSISTANT, Routes.ASSISTANT_MY_TASKS),
Components.JOB_POSTING_ASSISTANT => new(Event.SEND_TO_JOB_POSTING_ASSISTANT, Routes.ASSISTANT_JOB_POSTING),
Components.DOCUMENT_ANALYSIS_ASSISTANT => new(Event.SEND_TO_DOCUMENT_ANALYSIS_ASSISTANT, Routes.ASSISTANT_DOCUMENT_ANALYSIS),
Components.SLIDE_BUILDER_ASSISTANT => new(Event.SEND_TO_SLIDE_BUILDER_ASSISTANT, Routes.ASSISTANT_SLIDE_BUILDER),
Components.VISUAL_BRIEFING_ASSISTANT => new(Event.SEND_TO_VISUAL_BRIEFING_ASSISTANT, Routes.ASSISTANT_VISUAL_BRIEFING),
Components.CHAT => new(Event.SEND_TO_CHAT, Routes.CHAT),
_ => new(Event.NONE, Routes.ASSISTANTS),
};
public ConfidenceLevel MinimumConfidence(SettingsManager settingsManager) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.ConfigurationData.RewriteImprove.MinimumProviderConfidence : default,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.ConfigurationData.PromptOptimizer.MinimumProviderConfidence : default,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.ConfigurationData.Translation.MinimumProviderConfidence : default,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.ConfigurationData.Agenda.MinimumProviderConfidence : default,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.ConfigurationData.Coding.MinimumProviderConfidence : default,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.ConfigurationData.TextSummarizer.MinimumProviderConfidence : default,
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.ConfigurationData.SWOTAnalysis.MinimumProviderConfidence : default,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.ConfigurationData.EMail.MinimumProviderConfidence : default,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.ConfigurationData.LegalCheck.MinimumProviderConfidence : default,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.ConfigurationData.Synonyms.MinimumProviderConfidence : default,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.ConfigurationData.MyTasks.MinimumProviderConfidence : default,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.ConfigurationData.JobPostings.MinimumProviderConfidence : default,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.ConfigurationData.BiasOfTheDay.MinimumProviderConfidence : default,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.ConfigurationData.ERI.MinimumProviderConfidence : default,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.ConfigurationData.SlideBuilder.MinimumProviderConfidence : default,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.ConfigurationData.AssistantBuilder.MinimumProviderConfidence : default,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.ConfigurationData.VisualBriefing.MinimumProviderConfidence,
// The minimum confidence for the Document Analysis Assistant is set per policy.
// We do this inside the Document Analysis Assistant component:
Components.DOCUMENT_ANALYSIS_ASSISTANT => ConfidenceLevel.NONE,
// A policy-specific minimum is merged with this component default inside
// the Batch Processing Assistant; the stricter level wins.
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.ConfigurationData.BatchProcessing.MinimumProviderConfidence : default,
_ => default,
};
public AIStudio.Settings.Provider PreselectedProvider(SettingsManager settingsManager) => component switch
{
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.GrammarSpelling.PreselectedProvider) : Settings.Provider.NONE,
Components.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
Components.REWRITE_ASSISTANT => settingsManager.ConfigurationData.RewriteImprove.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.RewriteImprove.PreselectedProvider) : Settings.Provider.NONE,
Components.PROMPT_OPTIMIZER_ASSISTANT => settingsManager.ConfigurationData.PromptOptimizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.PromptOptimizer.PreselectedProvider) : Settings.Provider.NONE,
Components.TRANSLATION_ASSISTANT => settingsManager.ConfigurationData.Translation.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Translation.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENDA_ASSISTANT => settingsManager.ConfigurationData.Agenda.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Agenda.PreselectedProvider) : Settings.Provider.NONE,
Components.CODING_ASSISTANT => settingsManager.ConfigurationData.Coding.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Coding.PreselectedProvider) : Settings.Provider.NONE,
Components.TEXT_SUMMARIZER_ASSISTANT => settingsManager.ConfigurationData.TextSummarizer.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextSummarizer.PreselectedProvider) : Settings.Provider.NONE,
Components.SWOT_ANALYSIS_ASSISTANT => settingsManager.ConfigurationData.SWOTAnalysis.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SWOTAnalysis.PreselectedProvider) : Settings.Provider.NONE,
Components.EMAIL_ASSISTANT => settingsManager.ConfigurationData.EMail.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.EMail.PreselectedProvider) : Settings.Provider.NONE,
Components.LEGAL_CHECK_ASSISTANT => settingsManager.ConfigurationData.LegalCheck.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.LegalCheck.PreselectedProvider) : Settings.Provider.NONE,
Components.SYNONYMS_ASSISTANT => settingsManager.ConfigurationData.Synonyms.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.Synonyms.PreselectedProvider) : Settings.Provider.NONE,
Components.MY_TASKS_ASSISTANT => settingsManager.ConfigurationData.MyTasks.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.MyTasks.PreselectedProvider) : Settings.Provider.NONE,
Components.JOB_POSTING_ASSISTANT => settingsManager.ConfigurationData.JobPostings.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.JobPostings.PreselectedProvider) : Settings.Provider.NONE,
Components.BIAS_DAY_ASSISTANT => settingsManager.ConfigurationData.BiasOfTheDay.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BiasOfTheDay.PreselectedProvider) : Settings.Provider.NONE,
Components.ERI_ASSISTANT => settingsManager.ConfigurationData.ERI.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.ERI.PreselectedProvider) : Settings.Provider.NONE,
Components.I18N_ASSISTANT => settingsManager.ConfigurationData.I18N.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.I18N.PreselectedProvider) : Settings.Provider.NONE,
Components.SLIDE_BUILDER_ASSISTANT => settingsManager.ConfigurationData.SlideBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.SlideBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.META_ASSISTANT => settingsManager.ConfigurationData.AssistantBuilder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantBuilder.PreselectedProvider) : Settings.Provider.NONE,
Components.VISUAL_BRIEFING_ASSISTANT => settingsManager.GetProviderById(settingsManager.ConfigurationData.VisualBriefing.PreselectedProvider),
// The Document Analysis Assistant does not have a preselected provider at the component level.
// The provider is selected per policy instead. We do this inside the Document Analysis Assistant component.
Components.DOCUMENT_ANALYSIS_ASSISTANT => Settings.Provider.NONE,
Components.BATCH_PROCESSING_ASSISTANT => settingsManager.ConfigurationData.BatchProcessing.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.BatchProcessing.PreselectedProvider) : Settings.Provider.NONE,
// 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.GetProviderById(settingsManager.ConfigurationData.Chat.PreselectedProvider) : Settings.Provider.NONE,
Components.AGENT_TEXT_CONTENT_CLEANER => settingsManager.ConfigurationData.TextContentCleaner.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.TextContentCleaner.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_DATA_SOURCE_SELECTION => settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentDataSourceSelection.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_RETRIEVAL_CONTEXT_VALIDATION => settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectAgentOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.AgentRetrievalContextValidation.PreselectedAgentProvider) : Settings.Provider.NONE,
Components.AGENT_ASSISTANT_PLUGIN_AUDIT => settingsManager.GetProviderById(settingsManager.ConfigurationData.AssistantPluginAudit.PreselectedAgentProvider),
_ => Settings.Provider.NONE,
};
public ProfilePreselection GetProfilePreselection(SettingsManager settingsManager)
{
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,
// 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,
// 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,
_ => string.Empty,
};
return ProfilePreselection.FromStoredValue(storedValue);
}
public ChatTemplate PreselectedChatTemplate(SettingsManager settingsManager) => component switch
{
// 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.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE,
_ => ChatTemplate.NO_CHAT_TEMPLATE,
};
} }
public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch
{
// 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.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE,
_ => ChatTemplate.NO_CHAT_TEMPLATE,
};
} }
@@ -6,17 +6,20 @@ public static class ConfidenceSchemesExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceSchemesExtensions).Namespace, nameof(ConfidenceSchemesExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ConfidenceSchemesExtensions).Namespace, nameof(ConfidenceSchemesExtensions));
public static string GetListDescription(this ConfidenceSchemes scheme) => scheme switch extension(ConfidenceSchemes scheme)
{ {
ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"), public string GetListDescription() => scheme switch
ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"), {
ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"), ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"),
ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"), ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"),
ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"), ConfidenceSchemes.TRUST_USA => TB("Trust LLM providers from the USA"),
ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"), ConfidenceSchemes.TRUST_EUROPE => TB("Trust LLM providers from Europe"),
ConfidenceSchemes.TRUST_ASIA => TB("Trust LLM providers from Asia"),
ConfidenceSchemes.LOCAL_TRUST_ONLY => TB("Trust only local LLM providers"),
ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"), ConfidenceSchemes.CUSTOM => TB("Configure your own confidence scheme"),
_ => TB("Unknown confidence scheme"), _ => TB("Unknown confidence scheme"),
}; };
}
} }
@@ -2,66 +2,69 @@ namespace AIStudio.Tools;
public static class DirectoryInfoExtensions public static class DirectoryInfoExtensions
{ {
/// <summary>
/// Determines the size and file count of the files a selection takes from a directory. When
/// desired, it reports the found files up to a limit.
/// </summary>
/// <remarks>
/// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
/// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
/// to cancel slow operations, e.g., when the directory is on a slow network drive.
///
/// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
///
/// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
/// method to update the UI from a background thread.
/// </remarks>
/// <param name="directoryInfo">The root directory to determine the size of.</param> /// <param name="directoryInfo">The root directory to determine the size of.</param>
/// <param name="reportCurrentTotalSize">The callback to report the current total size of the directory.</param> extension(DirectoryInfo directoryInfo)
/// <param name="reportCurrentNumFiles">The callback to report the current number of files found.</param>
/// <param name="reportNextFile">The callback to report the next file found. The file name is relative to the root directory.</param>
/// <param name="reportMaxFiles">The maximum number of files to report. A negative value reports all files.</param>
/// <param name="done">The callback to report that the operation is done.</param>
/// <param name="cancellationToken">The cancellation token to stop the operation.</param>
/// <param name="selection">Which files count, e.g., those a data source indexes. Every file counts when it is missing.</param>
internal static async Task DetermineContentSize(this DirectoryInfo directoryInfo, Action<long> reportCurrentTotalSize, Action<long> reportCurrentNumFiles, Action<string> reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null)
{ {
var rootDirectoryLen = directoryInfo.FullName.Length; /// <summary>
var filesToCount = selection ?? FolderFileSelection.EVERY_FILE; /// Determines the size and file count of the files a selection takes from a directory. When
long totalSize = 0; /// desired, it reports the found files up to a limit.
long numFiles = 0; /// </summary>
/// <remarks>
/// You might set reportMaxFiles to a negative value to report all files. Any positive value will limit the number
/// of reported files. The cancellation token can be used to stop the operation. The cancellation operation is also able
/// to cancel slow operations, e.g., when the directory is on a slow network drive.
///
/// After stopping the operation, the total size and number of files are reported as they were at the time of cancellation.
///
/// Please note that the entire operation is done on a background thread. Thus, when reporting the found files or the
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync
/// method to update the UI from a background thread.
/// </remarks>
/// <param name="reportCurrentTotalSize">The callback to report the current total size of the directory.</param>
/// <param name="reportCurrentNumFiles">The callback to report the current number of files found.</param>
/// <param name="reportNextFile">The callback to report the next file found. The file name is relative to the root directory.</param>
/// <param name="reportMaxFiles">The maximum number of files to report. A negative value reports all files.</param>
/// <param name="done">The callback to report that the operation is done.</param>
/// <param name="cancellationToken">The cancellation token to stop the operation.</param>
/// <param name="selection">Which files count, e.g., those a data source indexes. Every file counts when it is missing.</param>
internal async Task DetermineContentSize(Action<long> reportCurrentTotalSize, Action<long> reportCurrentNumFiles, Action<string> reportNextFile, int reportMaxFiles = -1, Action? done = null, CancellationToken cancellationToken = default, FolderFileSelection? selection = null)
{
var rootDirectoryLen = directoryInfo.FullName.Length;
var filesToCount = selection ?? FolderFileSelection.EVERY_FILE;
long totalSize = 0;
long numFiles = 0;
await Task.Factory.StartNew(() => { await Task.Factory.StartNew(() => {
foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName)) foreach (var filePath in filesToCount.EnumerateCandidates(directoryInfo.FullName))
{
if (cancellationToken.IsCancellationRequested)
return;
if (!filesToCount.Selects(filePath))
continue;
var file = new FileInfo(filePath);
if (filesToCount.IsTooLarge(file))
continue;
totalSize += file.Length;
numFiles++;
if (numFiles % 100 == 0)
{ {
reportCurrentTotalSize(totalSize); if (cancellationToken.IsCancellationRequested)
reportCurrentNumFiles(numFiles); return;
if (!filesToCount.Selects(filePath))
continue;
var file = new FileInfo(filePath);
if (filesToCount.IsTooLarge(file))
continue;
totalSize += file.Length;
numFiles++;
if (numFiles % 100 == 0)
{
reportCurrentTotalSize(totalSize);
reportCurrentNumFiles(numFiles);
}
if (reportMaxFiles < 0 || numFiles <= reportMaxFiles)
reportNextFile(file.FullName[rootDirectoryLen..]);
} }
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
if (reportMaxFiles < 0 || numFiles <= reportMaxFiles) reportCurrentTotalSize(totalSize);
reportNextFile(file.FullName[rootDirectoryLen..]); reportCurrentNumFiles(numFiles);
}
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default);
reportCurrentTotalSize(totalSize); done?.Invoke();
reportCurrentNumFiles(numFiles); }
done?.Invoke();
} }
} }
@@ -6,12 +6,15 @@ public static class ProviderTypeExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderTypeExtensions).Namespace, nameof(ProviderTypeExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(ProviderTypeExtensions).Namespace, nameof(ProviderTypeExtensions));
public static string Explain(this ProviderType providerType) => providerType switch extension(ProviderType providerType)
{ {
ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."), public string Explain() => providerType switch
ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."), {
ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."), ProviderType.NONE => TB("The related data is not allowed to be sent to any LLM provider. This means that this data source cannot be used at the moment."),
ProviderType.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."),
ProviderType.SELF_HOSTED => TB("The related data can be sent to a provider that is hosted by the same organization, either on-premises or locally. Cloud-based providers are not allowed."),
_ => TB("Unknown configuration. This data source cannot be used at the moment."), _ => TB("Unknown configuration. This data source cannot be used at the moment."),
}; };
}
} }
@@ -4,8 +4,11 @@ namespace AIStudio.Tools;
public static class ElementReferenceExtensions public static class ElementReferenceExtensions
{ {
public static async ValueTask ScrollIntoViewAsync(this ElementReference elementReference, IJSRuntime jsRuntime) extension(ElementReference elementReference)
{ {
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference); public async ValueTask ScrollIntoViewAsync(IJSRuntime jsRuntime)
{
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference);
}
} }
} }
@@ -2,7 +2,13 @@ namespace AIStudio.Tools;
public static class EncryptedTextExtensions public static class EncryptedTextExtensions
{ {
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data); extension(string data)
{
public async Task<EncryptedText> Encrypt(Encryption encryption) => await encryption.Encrypt(data);
}
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText); extension(EncryptedText encryptedText)
{
public async Task<string> Decrypt(Encryption encryption) => await encryption.Decrypt(encryptedText);
}
} }
@@ -8,89 +8,95 @@ public static class ExpressionExtensions
{ {
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(typeof(ExpressionExtensions));
/// <summary>
/// Extracts the member expression from a given lambda expression representing a property.
/// </summary>
/// <param name="expression">A lambda expression specifying the property for which the member expression is to be extracted. /// <param name="expression">A lambda expression specifying the property for which the member expression is to be extracted.
/// The lambda expression body must represent member access.</param> /// The lambda expression body must represent member access.</param>
/// <typeparam name="TIn">The type of the object containing the property referenced in the lambda expression.</typeparam> /// <typeparam name="TIn">The type of the object containing the property referenced in the lambda expression.</typeparam>
/// <typeparam name="TOut">The type of the property being accessed in the lambda expression.</typeparam> /// <typeparam name="TOut">The type of the property being accessed in the lambda expression.</typeparam>
/// <returns>The member expression that represents the property access.</returns> extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression)
/// <exception cref="ArgumentException">Thrown if the provided lambda expression does not represent a valid property expression.</exception>
public static MemberExpression GetMemberExpression<TIn, TOut>(this Expression<Func<TIn, TOut>> expression)
{ {
switch (expression.Body) /// <summary>
/// Extracts the member expression from a given lambda expression representing a property.
/// </summary>
/// <returns>The member expression that represents the property access.</returns>
/// <exception cref="ArgumentException">Thrown if the provided lambda expression does not represent a valid property expression.</exception>
public MemberExpression GetMemberExpression()
{ {
// Case for value types, which are wrapped in UnaryExpression: switch (expression.Body)
case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression: {
return (MemberExpression)unaryExpression.Operand; // Case for value types, which are wrapped in UnaryExpression:
case UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression:
return (MemberExpression)unaryExpression.Operand;
// Case for reference types, which are directly MemberExpressions: // Case for reference types, which are directly MemberExpressions:
case MemberExpression memberExpression: case MemberExpression memberExpression:
return memberExpression; return memberExpression;
default: default:
LOGGER.LogError($"Expression '{expression}' is not a valid property expression."); LOGGER.LogError($"Expression '{expression}' is not a valid property expression.");
throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression)); throw new ArgumentException($"Expression '{expression}' is not a valid property expression.", nameof(expression));
}
} }
} }
/// <summary>
/// Attempts to increment the value of an uint property for a specified object using a
/// provided expression.
/// </summary>
/// <param name="expression">An expression representing the property to be incremented. The property /// <param name="expression">An expression representing the property to be incremented. The property
/// must be of type uint and belong to the provided object.</param> /// must be of type uint and belong to the provided object.</param>
/// <param name="data">The object that contains the property referenced by the expression.</param>
/// <param name="type">The type of increment operation to perform (e.g., prefix or postfix).</param>
/// <typeparam name="TIn">The type of the object that contains the property to be incremented.</typeparam> /// <typeparam name="TIn">The type of the object that contains the property to be incremented.</typeparam>
/// <typeparam name="TOut">The type of the property to be incremented.</typeparam> /// <typeparam name="TOut">The type of the property to be incremented.</typeparam>
/// <returns>An IncrementResult object containing the result of the increment operation.</returns> extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression) where TOut : IBinaryInteger<TOut>
public static IncrementResult<TOut> TryIncrement<TIn, TOut>(this Expression<Func<TIn, TOut>> expression, TIn data, IncrementType type) where TOut : IBinaryInteger<TOut>
{ {
// Ensure that the expression body is a member expression: /// <summary>
if (expression.Body is not MemberExpression memberExpression) /// Attempts to increment the value of an uint property for a specified object using a
return new(false, TOut.Zero); /// provided expression.
/// </summary>
// Ensure that the member expression is a property: /// <param name="data">The object that contains the property referenced by the expression.</param>
if (memberExpression.Member is not PropertyInfo propertyInfo) /// <param name="type">The type of increment operation to perform (e.g., prefix or postfix).</param>
return new(false, TOut.Zero); /// <returns>An IncrementResult object containing the result of the increment operation.</returns>
public IncrementResult<TOut> TryIncrement(TIn data, IncrementType type)
// Ensure that the member expression has a target object:
if (memberExpression.Expression is null)
return new(false, TOut.Zero);
// Get the target object for the expression, which is the object containing the property to increment:
var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters);
// Compile the lambda expression to get the target object
// (which is the object containing the property to increment):
var targetObject = targetObjectExpression.Compile().DynamicInvoke(data);
// Was the compilation successful?
if (targetObject is null)
return new(false, TOut.Zero);
// Read the current value of the property:
if (propertyInfo.GetValue(targetObject) is not TOut value)
return new(false, TOut.Zero);
// Increment the value:
switch (type)
{ {
case IncrementType.PRE: // Ensure that the expression body is a member expression:
var nextValue = value + TOut.CreateChecked(1); if (expression.Body is not MemberExpression memberExpression)
propertyInfo.SetValue(targetObject, nextValue);
return new(true, nextValue);
case IncrementType.POST:
var currentValue = value;
var incrementedValue = value + TOut.CreateChecked(1);
propertyInfo.SetValue(targetObject, incrementedValue);
return new(true, currentValue);
default:
return new(false, TOut.Zero); return new(false, TOut.Zero);
// Ensure that the member expression is a property:
if (memberExpression.Member is not PropertyInfo propertyInfo)
return new(false, TOut.Zero);
// Ensure that the member expression has a target object:
if (memberExpression.Expression is null)
return new(false, TOut.Zero);
// Get the target object for the expression, which is the object containing the property to increment:
var targetObjectExpression = Expression.Lambda(memberExpression.Expression, expression.Parameters);
// Compile the lambda expression to get the target object
// (which is the object containing the property to increment):
var targetObject = targetObjectExpression.Compile().DynamicInvoke(data);
// Was the compilation successful?
if (targetObject is null)
return new(false, TOut.Zero);
// Read the current value of the property:
if (propertyInfo.GetValue(targetObject) is not TOut value)
return new(false, TOut.Zero);
// Increment the value:
switch (type)
{
case IncrementType.PRE:
var nextValue = value + TOut.CreateChecked(1);
propertyInfo.SetValue(targetObject, nextValue);
return new(true, nextValue);
case IncrementType.POST:
var currentValue = value;
var incrementedValue = value + TOut.CreateChecked(1);
propertyInfo.SetValue(targetObject, incrementedValue);
return new(true, currentValue);
default:
return new(false, TOut.Zero);
}
} }
} }
} }
@@ -52,61 +52,215 @@ public static class FileExportFormatExtensions
/// </remarks> /// </remarks>
public static readonly IReadOnlyList<FileExportFormat> ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS]; public static readonly IReadOnlyList<FileExportFormat> ANSWER_FORMATS = [..DOCUMENT_FORMATS, ..TEXT_FORMATS];
/// <summary>
/// Returns the name of the format as shown to the user.
/// </summary>
/// <param name="format">The format.</param> /// <param name="format">The format.</param>
/// <returns>The name of the format.</returns> extension(FileExportFormat format)
public static string ToName(this FileExportFormat format) => format switch
{ {
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"), /// <summary>
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"), /// Returns the name of the format as shown to the user.
FileExportFormat.LATEX => TB("LaTeX (.tex)"), /// </summary>
FileExportFormat.MARKDOWN => TB("Markdown (.md)"), /// <returns>The name of the format.</returns>
FileExportFormat.HTML => TB("Webpage (.html)"), public string ToName() => format switch
FileExportFormat.CSV => TB("Table (.csv)"), {
FileExportFormat.TSV => TB("Table (.tsv)"), FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
FileExportFormat.SOURCE_CODE => TB("Source code"), FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
FileExportFormat.LATEX => TB("LaTeX (.tex)"),
FileExportFormat.MARKDOWN => TB("Markdown (.md)"),
FileExportFormat.HTML => TB("Webpage (.html)"),
FileExportFormat.CSV => TB("Table (.csv)"),
FileExportFormat.TSV => TB("Table (.tsv)"),
FileExportFormat.SOURCE_CODE => TB("Source code"),
_ => TB("Unknown format"), _ => TB("Unknown format"),
}; };
/// <summary> /// <summary>
/// Returns the icon of the format. /// Returns the icon of the format.
/// </summary> /// </summary>
/// <param name="format">The format.</param> /// <returns>The icon of the format.</returns>
/// <returns>The icon of the format.</returns> public string ToIcon() => format switch
public static string ToIcon(this FileExportFormat format) => format switch {
{ FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord,
FileExportFormat.MICROSOFT_WORD => Icons.Custom.FileFormats.FileWord, FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument,
FileExportFormat.OPEN_DOCUMENT_TEXT => Icons.Custom.FileFormats.FileDocument, FileExportFormat.LATEX => Icons.Material.Filled.Functions,
FileExportFormat.LATEX => Icons.Material.Filled.Functions, FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields,
FileExportFormat.MARKDOWN => Icons.Material.Filled.TextFields, FileExportFormat.HTML => Icons.Material.Filled.Html,
FileExportFormat.HTML => Icons.Material.Filled.Html, FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart,
FileExportFormat.CSV or FileExportFormat.TSV => Icons.Material.Filled.TableChart, FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code,
FileExportFormat.SOURCE_CODE => Icons.Material.Filled.Code,
_ => Icons.Material.Filled.Help, _ => Icons.Material.Filled.Help,
}; };
/// <summary> /// <summary>
/// Returns the file extension of the format, including the leading dot. /// Returns the file extension of the format, including the leading dot.
/// </summary> /// </summary>
/// <param name="format">The format.</param> /// <returns>The file extension, or an empty string when the format writes no file or, as code
/// <returns>The file extension, or an empty string when the format writes no file or, as code /// does, takes the extension from its language.</returns>
/// does, takes the extension from its language.</returns> public string ToFileExtension() => format switch
public static string ToFileExtension(this FileExportFormat format) => format switch {
{ FileExportFormat.MICROSOFT_WORD => ".docx",
FileExportFormat.MICROSOFT_WORD => ".docx", FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt", FileExportFormat.LATEX => ".tex",
FileExportFormat.LATEX => ".tex", FileExportFormat.MARKDOWN => ".md",
FileExportFormat.MARKDOWN => ".md", FileExportFormat.HTML => ".html",
FileExportFormat.HTML => ".html", FileExportFormat.CSV => ".csv",
FileExportFormat.CSV => ".csv", FileExportFormat.TSV => ".tsv",
FileExportFormat.TSV => ".tsv",
_ => string.Empty, _ => string.Empty,
}; };
/// <summary>
/// Determines whether the format holds a table rather than a text.
/// </summary>
/// <returns>True for the formats a spreadsheet opens.</returns>
public bool IsTabular() => format is FileExportFormat.CSV or FileExportFormat.TSV;
/// <summary>
/// Determines whether a file of the format is plain text, which AI Studio writes as it is.
/// </summary>
/// <remarks>
/// That holds for a web page and a LaTeX document as well, even though an entire answer needs
/// Pandoc to become one: the answer is Markdown, whereas a page the model wrote is a finished
/// file already. A Word or an OpenDocument file is an archive, and only Pandoc produces one. The
/// list is spelled out on purpose, so a format added later counts as plain text only once
/// somebody says so.
/// </remarks>
/// <returns>True, when a text written as it is makes a valid file of the format.</returns>
public bool IsPlainText() => format is FileExportFormat.LATEX or FileExportFormat.MARKDOWN or FileExportFormat.HTML or FileExportFormat.CSV or FileExportFormat.TSV or FileExportFormat.SOURCE_CODE;
/// <summary>
/// Returns the file name the save dialog starts with.
/// </summary>
/// <remarks>
/// Without a name, the dialog opens with an empty field and the user easily ends up with a
/// file which carries no extension at all. The fallback name is deliberately not translated:
/// a file name should survive being copied between systems and locales.
/// </remarks>
/// <param name="name">What the file is about, for example the heading above a table. Anything
/// a file name cannot hold is removed. Null or blank falls back to a generic name.</param>
/// <returns>The suggested file name, including its extension.</returns>
public string ToSuggestedFileName(string? name = null) => SuggestFileName(name, format.ToFileExtension());
/// <summary>
/// Returns the filter which the save dialog offers for the format.
/// </summary>
/// <returns>The filter, or null when the format cannot be written or, as code does, takes the
/// filter from its language.</returns>
public FileTypeFilter? ToFileTypeFilter() => format switch
{
FileExportFormat.MICROSOFT_WORD => FileTypes.MS_WORD,
FileExportFormat.OPEN_DOCUMENT_TEXT => FileTypes.ODT,
FileExportFormat.LATEX => FileTypes.TEX,
FileExportFormat.MARKDOWN => FileTypes.MARKDOWN,
FileExportFormat.HTML => FileTypes.HTML_DOCUMENT,
FileExportFormat.CSV => FileTypes.CSV,
FileExportFormat.TSV => FileTypes.TSV,
_ => null,
};
/// <summary>
/// Returns the encoding the file gets written with.
/// </summary>
/// <remarks>
/// Everything is UTF-8, the question is only whether the file starts with a byte order mark.
/// Tabular files get one, because Excel otherwise reads them in the local ANSI code page and
/// turns every umlaut into garbage. Text files get none: editors, compilers, and LaTeX have
/// no use for it and some of them stumble over it.
/// </remarks>
/// <returns>The encoding to write the file with.</returns>
public Encoding ToFileEncoding() => format switch
{
FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK,
_ => WITHOUT_BYTE_ORDER_MARK,
};
/// <summary>
/// Wraps a text into a comment of the format: whoever opens the file in an editor reads it,
/// while a browser or a LaTeX run skips it.
/// </summary>
/// <remarks>
/// A comment in HTML, and so in Markdown, ends at the first --> it holds, and a browser takes
/// --!> for the same; the rest of the text would spill onto the page from there. The title of
/// a web page may hold either, so a space goes in before the bracket, which keeps the text
/// readable and ends nothing. Every other pair of dashes stays, because a web address may carry
/// one, as in the xn-- of a domain with an umlaut. A LaTeX comment has no end to watch for: it
/// runs to the end of its line, so every line starts one.
/// </remarks>
/// <param name="text">The text to put into the comment.</param>
/// <param name="comment">The comment, or an empty string when the format has none.</param>
/// <returns>True, when the format knows comments.</returns>
public bool TryToComment(string text, out string comment)
{
var lines = text.TrimEnd().ReplaceLineEndings("\n").Split('\n');
switch (format)
{
case FileExportFormat.HTML or FileExportFormat.MARKDOWN:
var commentText = string.Join(Environment.NewLine, lines).Replace("-->", "-- >").Replace("--!>", "--! >");
comment = $"<!--{Environment.NewLine}{commentText}{Environment.NewLine}-->";
return true;
case FileExportFormat.LATEX:
comment = string.Join(Environment.NewLine, lines.Select(line => line.Length is 0 ? "%" : $"% {line}"));
return true;
default:
comment = string.Empty;
return false;
}
}
/// <summary>
/// Determines whether a link into a local file may name the page it points at.
/// </summary>
/// <remarks>
/// A page is named by the fragment of the link, the way the PDF open parameters call for. A
/// browser and a PDF reader follow that and open the document on the page; Word and LibreOffice
/// take the fragment for part of the file name, look for a file which does not exist, and refuse
/// the link altogether. There the page is dropped, so the link at least opens the document --
/// which page it was stays in the title of the source. Verified on 2026-09-15 with LibreOffice
/// on an exported .odt. A format added later keeps the page unless it is known to stumble too.
/// </remarks>
/// <returns>True, when a reader of this format follows such a link.</returns>
public bool FollowsPageAnchors() => format switch
{
FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false,
_ => true,
};
/// <summary>
/// Determines whether Pandoc has to be told the title of a document in the format.
/// </summary>
/// <remarks>
/// A web page shows its title in the browser tab. Without one, Pandoc names the page after its
/// input file, which is a temporary file of ours with a random name. Word and OpenDocument show
/// no such title, and handed one anyway, they keep it as a document property nobody asked for;
/// verified with Pandoc 3.8.3 on 2026-09-23. LaTeX ignores it.
/// </remarks>
/// <returns>True, when a document of this format needs a title besides its content.</returns>
public bool NeedsPageTitle() => format is FileExportFormat.HTML;
/// <summary>
/// Returns the name Pandoc knows the format by.
/// </summary>
/// <returns>The Pandoc output format, or an empty string when AI Studio writes the file itself.</returns>
public string ToPandocOutputFormat() => format switch
{
FileExportFormat.MICROSOFT_WORD => "docx",
FileExportFormat.OPEN_DOCUMENT_TEXT => "odt",
FileExportFormat.LATEX => "latex",
FileExportFormat.HTML => "html",
_ => string.Empty,
};
/// <summary>
/// Determines whether writing the format needs Pandoc.
/// </summary>
/// <returns>True, when Pandoc converts the message; false, when AI Studio writes the file itself.</returns>
public bool UsesPandoc() => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat());
}
/// <summary> /// <summary>
/// Reads which format a model means when it names a language behind the opening fence of a /// Reads which format a model means when it names a language behind the opening fence of a
@@ -139,41 +293,6 @@ public static class FileExportFormatExtensions
return format is not FileExportFormat.NONE; return format is not FileExportFormat.NONE;
} }
/// <summary>
/// Determines whether the format holds a table rather than a text.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>True for the formats a spreadsheet opens.</returns>
public static bool IsTabular(this FileExportFormat format) => format is FileExportFormat.CSV or FileExportFormat.TSV;
/// <summary>
/// Determines whether a file of the format is plain text, which AI Studio writes as it is.
/// </summary>
/// <remarks>
/// That holds for a web page and a LaTeX document as well, even though an entire answer needs
/// Pandoc to become one: the answer is Markdown, whereas a page the model wrote is a finished
/// file already. A Word or an OpenDocument file is an archive, and only Pandoc produces one. The
/// list is spelled out on purpose, so a format added later counts as plain text only once
/// somebody says so.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a text written as it is makes a valid file of the format.</returns>
public static bool IsPlainText(this FileExportFormat format) => format is FileExportFormat.LATEX or FileExportFormat.MARKDOWN or FileExportFormat.HTML or FileExportFormat.CSV or FileExportFormat.TSV or FileExportFormat.SOURCE_CODE;
/// <summary>
/// Returns the file name the save dialog starts with.
/// </summary>
/// <remarks>
/// Without a name, the dialog opens with an empty field and the user easily ends up with a
/// file which carries no extension at all. The fallback name is deliberately not translated:
/// a file name should survive being copied between systems and locales.
/// </remarks>
/// <param name="format">The format.</param>
/// <param name="name">What the file is about, for example the heading above a table. Anything
/// a file name cannot hold is removed. Null or blank falls back to a generic name.</param>
/// <returns>The suggested file name, including its extension.</returns>
public static string ToSuggestedFileName(this FileExportFormat format, string? name = null) => SuggestFileName(name, format.ToFileExtension());
/// <summary> /// <summary>
/// Returns the file name the save dialog starts with, for a file with the given extension. /// Returns the file name the save dialog starts with, for a file with the given extension.
/// </summary> /// </summary>
@@ -236,132 +355,4 @@ public static class FileExportFormatExtensions
// A trailing dot makes a file invisible on Unix and is dropped by Windows: // A trailing dot makes a file invisible on Unix and is dropped by Windows:
return fragment.ToString().TrimEnd('.'); return fragment.ToString().TrimEnd('.');
} }
/// <summary>
/// Returns the filter which the save dialog offers for the format.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The filter, or null when the format cannot be written or, as code does, takes the
/// filter from its language.</returns>
public static FileTypeFilter? ToFileTypeFilter(this FileExportFormat format) => format switch
{
FileExportFormat.MICROSOFT_WORD => FileTypes.MS_WORD,
FileExportFormat.OPEN_DOCUMENT_TEXT => FileTypes.ODT,
FileExportFormat.LATEX => FileTypes.TEX,
FileExportFormat.MARKDOWN => FileTypes.MARKDOWN,
FileExportFormat.HTML => FileTypes.HTML_DOCUMENT,
FileExportFormat.CSV => FileTypes.CSV,
FileExportFormat.TSV => FileTypes.TSV,
_ => null,
};
/// <summary>
/// Returns the encoding the file gets written with.
/// </summary>
/// <remarks>
/// Everything is UTF-8, the question is only whether the file starts with a byte order mark.
/// Tabular files get one, because Excel otherwise reads them in the local ANSI code page and
/// turns every umlaut into garbage. Text files get none: editors, compilers, and LaTeX have
/// no use for it and some of them stumble over it.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>The encoding to write the file with.</returns>
public static Encoding ToFileEncoding(this FileExportFormat format) => format switch
{
FileExportFormat.CSV or FileExportFormat.TSV => WITH_BYTE_ORDER_MARK,
_ => WITHOUT_BYTE_ORDER_MARK,
};
/// <summary>
/// Wraps a text into a comment of the format: whoever opens the file in an editor reads it,
/// while a browser or a LaTeX run skips it.
/// </summary>
/// <remarks>
/// A comment in HTML, and so in Markdown, ends at the first --> it holds, and a browser takes
/// --!> for the same; the rest of the text would spill onto the page from there. The title of
/// a web page may hold either, so a space goes in before the bracket, which keeps the text
/// readable and ends nothing. Every other pair of dashes stays, because a web address may carry
/// one, as in the xn-- of a domain with an umlaut. A LaTeX comment has no end to watch for: it
/// runs to the end of its line, so every line starts one.
/// </remarks>
/// <param name="format">The format.</param>
/// <param name="text">The text to put into the comment.</param>
/// <param name="comment">The comment, or an empty string when the format has none.</param>
/// <returns>True, when the format knows comments.</returns>
public static bool TryToComment(this FileExportFormat format, string text, out string comment)
{
var lines = text.TrimEnd().ReplaceLineEndings("\n").Split('\n');
switch (format)
{
case FileExportFormat.HTML or FileExportFormat.MARKDOWN:
var commentText = string.Join(Environment.NewLine, lines).Replace("-->", "-- >").Replace("--!>", "--! >");
comment = $"<!--{Environment.NewLine}{commentText}{Environment.NewLine}-->";
return true;
case FileExportFormat.LATEX:
comment = string.Join(Environment.NewLine, lines.Select(line => line.Length is 0 ? "%" : $"% {line}"));
return true;
default:
comment = string.Empty;
return false;
}
}
/// <summary>
/// Determines whether a link into a local file may name the page it points at.
/// </summary>
/// <remarks>
/// A page is named by the fragment of the link, the way the PDF open parameters call for. A
/// browser and a PDF reader follow that and open the document on the page; Word and LibreOffice
/// take the fragment for part of the file name, look for a file which does not exist, and refuse
/// the link altogether. There the page is dropped, so the link at least opens the document --
/// which page it was stays in the title of the source. Verified on 2026-09-15 with LibreOffice
/// on an exported .odt. A format added later keeps the page unless it is known to stumble too.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a reader of this format follows such a link.</returns>
public static bool FollowsPageAnchors(this FileExportFormat format) => format switch
{
FileExportFormat.MICROSOFT_WORD or FileExportFormat.OPEN_DOCUMENT_TEXT => false,
_ => true,
};
/// <summary>
/// Determines whether Pandoc has to be told the title of a document in the format.
/// </summary>
/// <remarks>
/// A web page shows its title in the browser tab. Without one, Pandoc names the page after its
/// input file, which is a temporary file of ours with a random name. Word and OpenDocument show
/// no such title, and handed one anyway, they keep it as a document property nobody asked for;
/// verified with Pandoc 3.8.3 on 2026-09-23. LaTeX ignores it.
/// </remarks>
/// <param name="format">The format.</param>
/// <returns>True, when a document of this format needs a title besides its content.</returns>
public static bool NeedsPageTitle(this FileExportFormat format) => format is FileExportFormat.HTML;
/// <summary>
/// Returns the name Pandoc knows the format by.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>The Pandoc output format, or an empty string when AI Studio writes the file itself.</returns>
public static string ToPandocOutputFormat(this FileExportFormat format) => format switch
{
FileExportFormat.MICROSOFT_WORD => "docx",
FileExportFormat.OPEN_DOCUMENT_TEXT => "odt",
FileExportFormat.LATEX => "latex",
FileExportFormat.HTML => "html",
_ => string.Empty,
};
/// <summary>
/// Determines whether writing the format needs Pandoc.
/// </summary>
/// <param name="format">The format.</param>
/// <returns>True, when Pandoc converts the message; false, when AI Studio writes the file itself.</returns>
public static bool UsesPandoc(this FileExportFormat format) => !string.IsNullOrWhiteSpace(format.ToPandocOutputFormat());
} }
@@ -15,98 +15,99 @@ internal static class FileExtractionErrorCodeExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionErrorCodeExtensions).Namespace, nameof(FileExtractionErrorCodeExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionErrorCodeExtensions).Namespace, nameof(FileExtractionErrorCodeExtensions));
/// <summary>
/// Gets a value indicating whether reading the file again will fail again, as long as the file
/// itself does not change.
/// </summary>
/// <param name="code">The stable failure code.</param> /// <param name="code">The stable failure code.</param>
/// <returns>True, when the reason lies in the file itself.</returns> extension(FileExtractionErrorCode code)
internal static bool IsPermanentIndexingFailure(this FileExtractionErrorCode code) => code switch
{ {
// /// <summary>
// The reason lies in the file. Reading it again without changing it produces the same /// Gets a value indicating whether reading the file again will fail again, as long as the file
// outcome, so the indexer waits for a new fingerprint: /// itself does not change.
// /// </summary>
FileExtractionErrorCode.NO_TEXT_EXTRACTED => true, /// <returns>True, when the reason lies in the file itself.</returns>
FileExtractionErrorCode.NO_CONTENT => true, internal bool IsPermanentIndexingFailure() => code switch
FileExtractionErrorCode.NOT_TEXT_CONTENT => true, {
FileExtractionErrorCode.NOT_A_VALID_PDF => true, //
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true, // The reason lies in the file. Reading it again without changing it produces the same
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true, // outcome, so the indexer waits for a new fingerprint:
FileExtractionErrorCode.PDF_ENCRYPTED => true, //
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true, FileExtractionErrorCode.NO_TEXT_EXTRACTED => true,
FileExtractionErrorCode.EXECUTABLE_REJECTED => true, FileExtractionErrorCode.NO_CONTENT => true,
FileExtractionErrorCode.UNSUPPORTED => true, FileExtractionErrorCode.NOT_TEXT_CONTENT => true,
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true, FileExtractionErrorCode.NOT_A_VALID_PDF => true,
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => true,
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => true,
FileExtractionErrorCode.PDF_ENCRYPTED => true,
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => true,
FileExtractionErrorCode.EXECUTABLE_REJECTED => true,
FileExtractionErrorCode.UNSUPPORTED => true,
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => true,
// Pages holding nothing but images are one of the recurring cases here, and that is a // Pages holding nothing but images are one of the recurring cases here, and that is a
// property of the document, not of the environment: // property of the document, not of the environment:
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true, FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => true,
// //
// Everything else depends on the surroundings: an unavailable drive, a file someone else // Everything else depends on the surroundings: an unavailable drive, a file someone else
// has open, a missing engine, or a runtime which did not answer in time. All of them are // has open, a missing engine, or a runtime which did not answer in time. All of them are
// worth another attempt during the next run: // worth another attempt during the next run:
// //
_ => false, _ => false,
}; };
/// <summary> /// <summary>
/// Names the cause in a few words. /// Names the cause in a few words.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Used to group the files of an indexing run by what happened to them: nine hundred entries /// Used to group the files of an indexing run by what happened to them: nine hundred entries
/// which all say the same sentence are one cause, not nine hundred. /// which all say the same sentence are one cause, not nine hundred.
/// </remarks> /// </remarks>
/// <param name="code">The stable failure code.</param> /// <returns>The localized name of the cause, or an empty text when the code has none.</returns>
/// <returns>The localized name of the cause, or an empty text when the code has none.</returns> internal string GetIndexingCauseName() => code switch
internal static string GetIndexingCauseName(this FileExtractionErrorCode code) => code switch {
{ FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"),
FileExtractionErrorCode.NO_TEXT_EXTRACTED => TB("No readable text"), FileExtractionErrorCode.NO_CONTENT => TB("No content"),
FileExtractionErrorCode.NO_CONTENT => TB("No content"), FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"),
FileExtractionErrorCode.NOT_TEXT_CONTENT => TB("Not a text file"), FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"),
FileExtractionErrorCode.NOT_A_VALID_PDF => TB("Not a readable PDF"), FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"),
FileExtractionErrorCode.NOT_A_VALID_SPREADSHEET => TB("Not a readable spreadsheet"), FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"),
FileExtractionErrorCode.NOT_A_VALID_DOCUMENT => TB("Not a readable document"), FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"),
FileExtractionErrorCode.PDF_ENCRYPTED => TB("Protected PDF"), FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"),
FileExtractionErrorCode.FORMAT_DETECTION_FAILED => TB("Unknown file type"), FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"),
FileExtractionErrorCode.EXECUTABLE_REJECTED => TB("Executable program"), FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"),
FileExtractionErrorCode.UNSUPPORTED => TB("Unsupported file type"), FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"),
FileExtractionErrorCode.MEDIA_PROCESSING_FAILED => TB("Media file could not be processed"), FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"),
FileExtractionErrorCode.PAGE_EXTRACTION_FAILED => TB("Pages without readable text"),
FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"), FileExtractionErrorCode.FILE_NOT_FOUND => TB("File does not exist anymore"),
FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"), FileExtractionErrorCode.FILE_NOT_READABLE => TB("File could not be read"),
FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"), FileExtractionErrorCode.FILE_LOCKED => TB("File is open elsewhere"),
FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"), FileExtractionErrorCode.TIMEOUT => TB("Reading took too long"),
FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"), FileExtractionErrorCode.PDFIUM_UNAVAILABLE => TB("PDF system unavailable"),
FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"), FileExtractionErrorCode.PANDOC_UNAVAILABLE => TB("Pandoc unavailable"),
// Nothing about these lies in the file: AI Studio asked its runtime for the content and // Nothing about these lies in the file: AI Studio asked its runtime for the content and
// got back something it cannot work with. One name for all of them, because that is the // got back something it cannot work with. One name for all of them, because that is the
// one thing the user can tell from them: // one thing the user can tell from them:
FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"), FileExtractionErrorCode.INVALID_RESPONSE => TB("Internal error"),
FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"), FileExtractionErrorCode.INVALID_REQUEST => TB("Internal error"),
FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"), FileExtractionErrorCode.REQUEST_FAILED => TB("Internal error"),
FileExtractionErrorCode.INTERNAL => TB("Internal error"), FileExtractionErrorCode.INTERNAL => TB("Internal error"),
// Codes which say nothing beyond the message of the single file. The caller names those // Codes which say nothing beyond the message of the single file. The caller names those
// files itself and shows their messages instead: // files itself and shows their messages instead:
_ => string.Empty, _ => string.Empty,
}; };
/// <summary> /// <summary>
/// Gets the localized message which explains why a file was not indexed. /// Gets the localized message which explains why a file was not indexed.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// These texts are the counterpart of the ones used for chat attachments: there, a file which /// These texts are the counterpart of the ones used for chat attachments: there, a file which
/// cannot be read is simply not sent, while here it stays out of the index and the user needs /// cannot be read is simply not sent, while here it stays out of the index and the user needs
/// to know whether AI Studio will come back to it on its own. /// to know whether AI Studio will come back to it on its own.
/// </remarks> /// </remarks>
/// <param name="code">The stable failure code.</param> /// <param name="fileName">The name of the file, as shown to the user.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param> /// <returns>The localized message.</returns>
/// <returns>The localized message.</returns> internal string ToIndexingUserMessage(string fileName) => string.Format(ToIndexingMessageFormat(code), fileName);
internal static string ToIndexingUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToIndexingMessageFormat(code), fileName); }
private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch
{ {
@@ -14,60 +14,64 @@ internal static class FileExtractionResultExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(FileExtractionResultExtensions).Namespace, nameof(FileExtractionResultExtensions));
/// <summary>
/// Gets the localized message which explains why a file could not be read.
/// </summary>
/// <param name="result">The extraction result.</param> /// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param> extension(FileExtractionResult result)
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this FileExtractionResult result, string fileName)
{ {
// When we know what the file really is, naming it beats a generic "not supported": /// <summary>
if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null) /// Gets the localized message which explains why a file could not be read.
return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat); /// </summary>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal string ToUserMessage(string fileName)
{
// When we know what the file really is, naming it beats a generic "not supported":
if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null)
return string.Format(TB("The file '{0}' is a {1}, which AI Studio cannot read, so it was not sent."), fileName, result.DetectedFormat);
return result.ErrorCode.ToUserMessage(fileName); return result.ErrorCode.ToUserMessage(fileName);
}
/// <summary>
/// Gets the localized message for a file whose content does not match its file extension.
/// </summary>
/// <remarks>
/// This is a notice, not a failure: the file was read according to its content. We still tell
/// the user, because a wrong extension is a real problem for every other program as well.
/// </remarks>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal string ToExtensionMismatchUserMessage(string fileName) => string.Format(
TB("The file '{0}' is actually a {1} and was read as such. Please correct its file extension."),
fileName,
result.DetectedFormat);
/// <summary>
/// Gets the localized message for a file which was read, but lost some of its pages.
/// </summary>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal string ToPartialUserMessage(string fileName)
{
if (result.FailedPages.Count == 0)
return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName);
return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages));
}
} }
/// <summary>
/// Gets the localized message for a file whose content does not match its file extension.
/// </summary>
/// <remarks>
/// This is a notice, not a failure: the file was read according to its content. We still tell
/// the user, because a wrong extension is a real problem for every other program as well.
/// </remarks>
/// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToExtensionMismatchUserMessage(this FileExtractionResult result, string fileName) => string.Format(
TB("The file '{0}' is actually a {1} and was read as such. Please correct its file extension."),
fileName,
result.DetectedFormat);
/// <summary>
/// Gets the localized message which explains why a file could not be read.
/// </summary>
/// <remarks>
/// This overload exists for the places which know the reason before an extraction was even
/// attempted, so both ways of skipping a file tell the user the same thing.
/// </remarks>
/// <param name="code">The stable failure code.</param> /// <param name="code">The stable failure code.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param> extension(FileExtractionErrorCode code)
/// <returns>The localized message.</returns>
internal static string ToUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName);
/// <summary>
/// Gets the localized message for a file which was read, but lost some of its pages.
/// </summary>
/// <param name="result">The extraction result.</param>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal static string ToPartialUserMessage(this FileExtractionResult result, string fileName)
{ {
if (result.FailedPages.Count == 0) /// <summary>
return string.Format(TB("Parts of the file '{0}' could not be read. The remaining content was sent."), fileName); /// Gets the localized message which explains why a file could not be read.
/// </summary>
return string.Format(TB("The pages {1} of the file '{0}' could not be read. The remaining content was sent."), fileName, string.Join(", ", result.FailedPages)); /// <remarks>
/// This overload exists for the places which know the reason before an extraction was even
/// attempted, so both ways of skipping a file tell the user the same thing.
/// </remarks>
/// <param name="fileName">The name of the file, as shown to the user.</param>
/// <returns>The localized message.</returns>
internal string ToUserMessage(string fileName) => string.Format(ToUserMessageFormat(code), fileName);
} }
private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch
@@ -2,16 +2,19 @@ namespace AIStudio.Tools;
public static class FileInfoExtensions public static class FileInfoExtensions
{ {
/// <summary>
/// Returns the file size in human-readable format.
/// </summary>
/// <param name="fileInfo">The file info object.</param> /// <param name="fileInfo">The file info object.</param>
/// <returns>The file size in human-readable format.</returns> extension(FileInfo fileInfo)
public static string FileSize(this FileInfo fileInfo)
{ {
if (!fileInfo.Exists) /// <summary>
return "N/A"; /// Returns the file size in human-readable format.
/// </summary>
/// <returns>The file size in human-readable format.</returns>
public string FileSize()
{
if (!fileInfo.Exists)
return "N/A";
return fileInfo.Length.FileSize(); return fileInfo.Length.FileSize();
}
} }
} }
@@ -11,8 +11,11 @@ public static class HttpRequestHeadersExtensions
API_TOKEN = Program.API_TOKEN; API_TOKEN = Program.API_TOKEN;
} }
public static void AddApiToken(this HttpRequestHeaders headers) extension(HttpRequestHeaders headers)
{ {
headers.Add("token", API_TOKEN); public void AddApiToken()
{
headers.Add("token", API_TOKEN);
}
} }
} }
@@ -4,107 +4,110 @@ public static class IConfidenceExtensions
{ {
private static readonly ILogger<IConfidence> LOGGER = Program.LOGGER_FACTORY.CreateLogger<IConfidence>(); private static readonly ILogger<IConfidence> LOGGER = Program.LOGGER_FACTORY.CreateLogger<IConfidence>();
public static TargetWindow DetermineTargetWindow<T>(this IReadOnlyList<T> items, TargetWindowStrategy strategy, int numMaximumItems = 30) where T : IConfidence
{
switch (strategy)
{
case TargetWindowStrategy.A_FEW_GOOD_ONES:
return new(1, 2, 3, 0f);
case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING:
var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f);
if(numItemsBetterThanGuessing < 3)
return new(1, 2, 3, 0.5f);
// We want the top 10% of items better than guessing:
var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f);
// When these 10% are just a few items, we take them all:
if (numTop10Percent <= 10)
{
var diff = numItemsBetterThanGuessing - numTop10Percent;
var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f);
return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f);
}
// Let's define the size of the window:
const int MIN_NUM_ITEMS = 3;
var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent);
windowMin = Math.Min(windowMin, numMaximumItems - 1);
var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3);
var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent));
var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize);
return new(totalMin, windowMin, windowMax, 0.5f);
case TargetWindowStrategy.NONE:
default:
return new(-1, -1, -1, 0f);
}
}
/// <summary>
/// Determine the optimal confidence threshold for a list of items
/// in order to match a target window of number of items.
/// </summary>
/// <param name="items">The list of confidence items to analyze.</param> /// <param name="items">The list of confidence items to analyze.</param>
/// <param name="targetWindow">The target window for the number of items.</param>
/// <param name="maxSteps">The maximum number of steps to search for the threshold.</param>
/// <typeparam name="T">The type of items in the list.</typeparam> /// <typeparam name="T">The type of items in the list.</typeparam>
/// <returns>The confidence threshold.</returns> extension<T>(IReadOnlyList<T> items) where T : IConfidence
public static float GetConfidenceThreshold<T>(this IReadOnlyList<T> items, TargetWindow targetWindow, int maxSteps = 10) where T : IConfidence
{ {
if(!targetWindow.IsValid()) public TargetWindow DetermineTargetWindow(TargetWindowStrategy strategy, int numMaximumItems = 30)
{ {
LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold."); switch (strategy)
return 0f;
}
//
// Without items there is no threshold to find, and the Min and Max calls below would throw
// on an empty sequence. Every caller checks this today, which is precisely how such a guard
// goes missing once a new caller arrives. It belongs here, next to the calls it protects:
//
if(items.Count == 0)
{
LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold.");
return 0f;
}
var confidenceValues = items.Select(x => x.Confidence).ToList();
var minConfidence = confidenceValues.Min();
var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold);
var upperBound = confidenceValues.Max();
//
// We search for a threshold so that we have between
// targetWindowMin and targetWindowMax items. When not
// possible, we take all items (e.g., threshold = 0f; depends on the used window strategy)
//
var threshold = 0.0f;
// Check the case where the confidence values are too close:
if (upperBound - minConfidence >= 0.01)
{
var previousThreshold = threshold;
for (var i = 0; i < maxSteps; i++)
{ {
threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps; case TargetWindowStrategy.A_FEW_GOOD_ONES:
var numMatches = items.Count(x => x.Confidence >= threshold); return new(1, 2, 3, 0f);
if (numMatches <= targetWindow.NumMinItems)
{
threshold = previousThreshold;
break;
}
if (targetWindow.InsideWindow(numMatches)) case TargetWindowStrategy.TOP10_BETTER_THAN_GUESSING:
break; var numItemsBetterThanGuessing = items.Count(x => x.Confidence > 0.5f);
if(numItemsBetterThanGuessing < 3)
return new(1, 2, 3, 0.5f);
previousThreshold = threshold; // We want the top 10% of items better than guessing:
var numTop10Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.1f);
// When these 10% are just a few items, we take them all:
if (numTop10Percent <= 10)
{
var diff = numItemsBetterThanGuessing - numTop10Percent;
var num50Percent = (int) MathF.Floor(numItemsBetterThanGuessing * 0.5f);
return new(num50Percent, num50Percent + 1, Math.Max(numItemsBetterThanGuessing, diff), 0.5f);
}
// Let's define the size of the window:
const int MIN_NUM_ITEMS = 3;
var windowMin = Math.Max(MIN_NUM_ITEMS + 1, numTop10Percent);
windowMin = Math.Min(windowMin, numMaximumItems - 1);
var totalMin = Math.Max(MIN_NUM_ITEMS, windowMin - 3);
var windowSize = (int)MathF.Max(MathF.Floor(numTop10Percent * 0.1f), MathF.Min(10, numTop10Percent));
var windowMax = Math.Min(numMaximumItems, numTop10Percent + windowSize);
return new(totalMin, windowMin, windowMax, 0.5f);
case TargetWindowStrategy.NONE:
default:
return new(-1, -1, -1, 0f);
} }
} }
else
LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold.");
return threshold; /// <summary>
/// Determine the optimal confidence threshold for a list of items
/// in order to match a target window of number of items.
/// </summary>
/// <param name="targetWindow">The target window for the number of items.</param>
/// <param name="maxSteps">The maximum number of steps to search for the threshold.</param>
/// <returns>The confidence threshold.</returns>
public float GetConfidenceThreshold(TargetWindow targetWindow, int maxSteps = 10)
{
if(!targetWindow.IsValid())
{
LOGGER.LogWarning("The target window is invalid. Returning 0f as threshold.");
return 0f;
}
//
// Without items there is no threshold to find, and the Min and Max calls below would throw
// on an empty sequence. Every caller checks this today, which is precisely how such a guard
// goes missing once a new caller arrives. It belongs here, next to the calls it protects:
//
if(items.Count == 0)
{
LOGGER.LogWarning("There are no items to determine a confidence threshold for. Returning 0f as threshold.");
return 0f;
}
var confidenceValues = items.Select(x => x.Confidence).ToList();
var minConfidence = confidenceValues.Min();
var lowerBound = MathF.Max(minConfidence, targetWindow.MinThreshold);
var upperBound = confidenceValues.Max();
//
// We search for a threshold so that we have between
// targetWindowMin and targetWindowMax items. When not
// possible, we take all items (e.g., threshold = 0f; depends on the used window strategy)
//
var threshold = 0.0f;
// Check the case where the confidence values are too close:
if (upperBound - minConfidence >= 0.01)
{
var previousThreshold = threshold;
for (var i = 0; i < maxSteps; i++)
{
threshold = lowerBound + (upperBound - lowerBound) * i / maxSteps;
var numMatches = items.Count(x => x.Confidence >= threshold);
if (numMatches <= targetWindow.NumMinItems)
{
threshold = previousThreshold;
break;
}
if (targetWindow.InsideWindow(numMatches))
break;
previousThreshold = threshold;
}
}
else
LOGGER.LogWarning("The confidence values are too close. Returning 0f as threshold.");
return threshold;
}
} }
} }
@@ -3,18 +3,27 @@ using AIStudio.Tools.Services;
namespace AIStudio.Tools; namespace AIStudio.Tools;
//
// The methods with a params parameter stay classic extension methods instead of moving into the
// extension block: for a member of an extension block, the compiler of the .NET 10.0.1xx SDK warns
// about every argument passed one by one to a params parameter (CS8620, dotnet/roslyn#80024). Move
// them once the SDK in metadata.txt carries the fix; MWAIS0016 makes the same exception until then.
//
public static class JsRuntimeExtensions public static class JsRuntimeExtensions
{ {
private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions)); private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(nameof(JsRuntimeExtensions));
public static async Task GenerateAndShowDiff(this IJSRuntime jsRuntime, string text1, string text2) extension(IJSRuntime jsRuntime)
{ {
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID); public async Task GenerateAndShowDiff(string text1, string text2)
} {
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID);
}
public static async Task ClearDiv(this IJSRuntime jsRuntime, string divId) public async Task ClearDiv(string divId)
{ {
await jsRuntime.InvokeVoidAsync("clearDiv", divId); await jsRuntime.InvokeVoidAsync("clearDiv", divId);
}
} }
/// <summary> /// <summary>
@@ -4,18 +4,21 @@ namespace AIStudio.Tools;
public static class JsonSerializerOptionsExtensions public static class JsonSerializerOptionsExtensions
{ {
/// <summary>
/// Makes the options read-only and returns them, so that a static field gets its options in one expression.
/// </summary>
/// <remarks>
/// Options with contracts of their own, i.e., with a TypeInfoResolver, should never change after they were
/// declared: whoever used them before such a change would have read or written with other contracts.
/// </remarks>
/// <param name="options">The options, with their TypeInfoResolver set.</param> /// <param name="options">The options, with their TypeInfoResolver set.</param>
/// <returns>The same options, now read-only.</returns> extension(JsonSerializerOptions options)
public static JsonSerializerOptions AsReadOnly(this JsonSerializerOptions options)
{ {
options.MakeReadOnly(); /// <summary>
return options; /// Makes the options read-only and returns them, so that a static field gets its options in one expression.
/// </summary>
/// <remarks>
/// Options with contracts of their own, i.e., with a TypeInfoResolver, should never change after they were
/// declared: whoever used them before such a change would have read or written with other contracts.
/// </remarks>
/// <returns>The same options, now read-only.</returns>
public JsonSerializerOptions AsReadOnly()
{
options.MakeReadOnly();
return options;
}
} }
} }
+61 -53
View File
@@ -8,67 +8,75 @@ public static class LongExtensions
private static readonly string[] SIZE_UNITS = ["B", "kB", "MB", "GB", "TB"]; private static readonly string[] SIZE_UNITS = ["B", "kB", "MB", "GB", "TB"];
/// <summary>
/// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
/// </summary>
/// <remarks>
/// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage
/// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the
/// difference between 4 and 999. The decimal separator follows the culture of the active language
/// plugin rather than the one of the thread, which never moves along with the app's language.
/// </remarks>
/// <param name="count">The number to format.</param> /// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns> extension(long count)
public static string CompactCount(this long count)
{ {
var culture = I18N.I.Culture; /// <summary>
if (count is > -1_000 and < 1_000) /// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
return count.ToString("N0", culture); /// </summary>
/// <remarks>
var order = -1; /// Counts scale in thousands, not in steps of 1024 — that is what FileSize is for, and storage
double value = count; /// sizes keep using it. Numbers below 1000 stay exact, because shortening them would hide the
while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1) /// difference between 4 and 999. The decimal separator follows the culture of the active language
/// plugin rather than the one of the thread, which never moves along with the app's language.
/// </remarks>
/// <returns>The formatted number.</returns>
public string CompactCount()
{ {
order++; var culture = I18N.I.Culture;
value /= 1_000; if (count is > -1_000 and < 1_000)
} return count.ToString("N0", culture);
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}"; var order = -1;
double value = count;
while (Math.Abs(value) >= 1_000 && order < COUNT_SUFFIXES.Length - 1)
{
order++;
value /= 1_000;
}
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}";
}
} }
/// <summary>
/// Formats a count so that large numbers stay readable.
/// </summary>
/// <param name="count">The number to format.</param> /// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns> extension(int count)
public static string CompactCount(this int count) => ((long)count).CompactCount();
/// <summary>
/// Formats the file size in a human-readable format, e.g., 1.9 MB.
/// </summary>
/// <remarks>
/// Like CompactCount, the decimal separator follows the culture of the active language plugin.
/// </remarks>
/// <param name="sizeBytes">The size in bytes.</param>
/// <returns>The formatted file size.</returns>
public static string FileSize(this long sizeBytes) => sizeBytes.FileSize(I18N.I.Culture);
/// <summary>
/// Formats the file size in a human-readable format, with the decimal separator of the given culture.
/// </summary>
/// <param name="sizeBytes">The size in bytes.</param>
/// <param name="culture">The culture whose decimal separator to use.</param>
/// <returns>The formatted file size.</returns>
internal static string FileSize(this long sizeBytes, IFormatProvider culture)
{ {
var order = 0; /// <summary>
double size = sizeBytes; /// Formats a count so that large numbers stay readable.
while (size >= 1024 && order < SIZE_UNITS.Length - 1) /// </summary>
{ /// <returns>The formatted number.</returns>
order++; public string CompactCount() => ((long)count).CompactCount();
size /= 1024; }
}
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}"; /// <param name="sizeBytes">The size in bytes.</param>
extension(long sizeBytes)
{
/// <summary>
/// Formats the file size in a human-readable format, e.g., 1.9 MB.
/// </summary>
/// <remarks>
/// Like CompactCount, the decimal separator follows the culture of the active language plugin.
/// </remarks>
/// <returns>The formatted file size.</returns>
public string FileSize() => sizeBytes.FileSize(I18N.I.Culture);
/// <summary>
/// Formats the file size in a human-readable format, with the decimal separator of the given culture.
/// </summary>
/// <param name="culture">The culture whose decimal separator to use.</param>
/// <returns>The formatted file size.</returns>
internal string FileSize(IFormatProvider culture)
{
var order = 0;
double size = sizeBytes;
while (size >= 1024 && order < SIZE_UNITS.Length - 1)
{
order++;
size /= 1024;
}
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}";
}
} }
} }
@@ -2,14 +2,17 @@ namespace AIStudio.Tools.MIME;
public static class MIMETypeExtensions public static class MIMETypeExtensions
{ {
public static string[] ToStringArray(this MIMEType[] mimeTypes) extension(MIMEType[] mimeTypes)
{ {
var result = new string[mimeTypes.Length]; public string[] ToStringArray()
for (var i = 0; i < mimeTypes.Length; i++)
{ {
result[i] = mimeTypes[i]; var result = new string[mimeTypes.Length];
} for (var i = 0; i < mimeTypes.Length; i++)
{
result[i] = mimeTypes[i];
}
return result; return result;
}
} }
} }
@@ -6,18 +6,21 @@ public static class MailboxConnectionFailureExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxConnectionFailureExtensions).Namespace, nameof(MailboxConnectionFailureExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxConnectionFailureExtensions).Namespace, nameof(MailboxConnectionFailureExtensions));
/// <summary> extension(MailboxConnectionFailure failure)
/// What went wrong, and what the user can do about it.
/// </summary>
public static string GetDescription(this MailboxConnectionFailure failure) => failure switch
{ {
MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."), /// <summary>
MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."), /// What went wrong, and what the user can do about it.
MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."), /// </summary>
MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."), public string GetDescription() => failure switch
MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."), {
MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."), MailboxConnectionFailure.AUTHENTICATION_FAILED => TB("The server rejected the username or the password. Some providers require an app password instead of your usual password."),
MailboxConnectionFailure.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."),
MailboxConnectionFailure.TLS_FAILED => TB("No encrypted connection to the server could be established. When your organization uses a certificate authority of its own, enable the additional root certificates in the app settings, select the bundle with its root certificate, and add the host of the server to the allowed hosts. Your IT department can also configure this for you."),
MailboxConnectionFailure.SERVER_ERROR => TB("The server reported an error. Please try again later."),
MailboxConnectionFailure.INVALID_SETTINGS => TB("The settings of this mailbox are incomplete, or they were made by a newer version of AI Studio."),
MailboxConnectionFailure.SERVER_NOT_ALLOWED => TB("Your organization allows mailboxes only on its own mail servers, and this server is none of them. AI Studio does not connect to it, and the AI does not read this mailbox."),
_ => TB("The connection to the server failed for an unknown reason."), _ => TB("The connection to the server failed for an unknown reason."),
}; };
}
} }
@@ -6,33 +6,39 @@ public static class MailboxProviderTemplateExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxProviderTemplateExtensions).Namespace, nameof(MailboxProviderTemplateExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(MailboxProviderTemplateExtensions).Namespace, nameof(MailboxProviderTemplateExtensions));
/// <summary> extension(MailboxUsernameFormat format)
/// What the user enters as the username at this provider.
/// </summary>
public static string GetDescription(this MailboxUsernameFormat format) => format switch
{ {
MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."), /// <summary>
MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."), /// What the user enters as the username at this provider.
MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"), /// </summary>
public string GetDescription() => format switch
{
MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."),
MailboxUsernameFormat.ADDRESS_NAME_PART => TB("The part of your e-mail address before the @ sign. When that does not work, try your full e-mail address."),
MailboxUsernameFormat.DOMAIN_ACCOUNT => string.Format(TB("Your account in the directory of your organization, either as {0} or as {1}. Your IT department knows which form your server expects."), @"DOMAIN\username", "username@domain"),
_ => TB("The username your provider gave you."), _ => TB("The username your provider gave you."),
}; };
}
/// <summary> extension(MailboxProviderRequirements requirements)
/// What the user has to do before AI Studio can sign in, one text per requirement.
/// </summary>
public static IEnumerable<string> GetDescriptions(this MailboxProviderRequirements requirements)
{ {
if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION)) /// <summary>
yield return TB("Enable the IMAP access in the settings of your webmail first. The provider may switch it off again after a longer time without use."); /// What the user has to do before AI Studio can sign in, one text per requirement.
/// </summary>
public IEnumerable<string> GetDescriptions()
{
if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION))
yield return TB("Enable the IMAP access in the settings of your webmail first. The provider may switch it off again after a longer time without use.");
if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION)) if (requirements.HasFlag(MailboxProviderRequirements.ADMIN_ACTIVATION))
yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first."); yield return TB("Your IT department has to enable IMAP for the server and for your mailbox first.");
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD)) if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD))
yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here."); yield return TB("This provider requires an app password, which you create in the security settings of your account. Your usual password does not work here.");
if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR)) if (requirements.HasFlag(MailboxProviderRequirements.APP_PASSWORD_WITH_TWO_FACTOR))
yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account."); yield return TB("When two-factor authentication is enabled for your account, this provider requires an app password, which you create in the security settings of your account.");
}
} }
} }
@@ -3,17 +3,20 @@ namespace AIStudio.Tools.Media;
/// <summary>Capabilities of a media-import owner kind.</summary> /// <summary>Capabilities of a media-import owner kind.</summary>
public static class MediaImportOwnerKindExtensions public static class MediaImportOwnerKindExtensions
{ {
/// <summary>
/// Gets whether the owner stores its own source list and transcripts.
/// </summary>
/// <remarks>
/// Owners that persist their own sources take the attached media over immediately and keep it
/// next to the stored document, see <see cref="MediaImportOwnerKind.VISUAL_BRIEFING"/>. The
/// attachment control must therefore neither wait for the transcription to finish before showing
/// the file, nor deliver the completed transcripts back into its own list afterwards, because
/// the owner already holds them. All other owners rely on that delivery instead.
/// </remarks>
/// <param name="kind">The owner kind to look up.</param> /// <param name="kind">The owner kind to look up.</param>
/// <returns><c>true</c> when the owner persists its own sources.</returns> extension(MediaImportOwnerKind kind)
public static bool PersistsOwnSources(this MediaImportOwnerKind kind) => kind is MediaImportOwnerKind.VISUAL_BRIEFING; {
/// <summary>
/// Gets whether the owner stores its own source list and transcripts.
/// </summary>
/// <remarks>
/// Owners that persist their own sources take the attached media over immediately and keep it
/// next to the stored document, see <see cref="MediaImportOwnerKind.VISUAL_BRIEFING"/>. The
/// attachment control must therefore neither wait for the transcription to finish before showing
/// the file, nor deliver the completed transcripts back into its own list afterwards, because
/// the owner already holds them. All other owners rely on that delivery instead.
/// </remarks>
/// <returns><c>true</c> when the owner persists its own sources.</returns>
public bool PersistsOwnSources() => kind is MediaImportOwnerKind.VISUAL_BRIEFING;
}
} }
@@ -4,13 +4,19 @@ namespace AIStudio.Tools;
public static class MessageBusExtensions public static class MessageBusExtensions
{ {
public static async Task SendMessage<T>(this ComponentBase component, Event triggeredEvent, T? data = default) extension(ComponentBase component)
{ {
await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data); public async Task SendMessage<T>(Event triggeredEvent, T? data = default)
{
await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data);
}
} }
public static void ApplyFilters(this IMessageBusReceiver component, ComponentBase[] components, Event[] events) extension(IMessageBusReceiver component)
{ {
MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet()); public void ApplyFilters(ComponentBase[] components, Event[] events)
{
MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet());
}
} }
} }
@@ -4,19 +4,22 @@ namespace AIStudio.Tools;
public static class MudThemeExtensions public static class MudThemeExtensions
{ {
public static Palette GetCurrentPalette(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch extension(MudTheme theme)
{ {
true => theme.PaletteDark, public Palette GetCurrentPalette(SettingsManager settingsManager) => settingsManager.IsDarkMode switch
false => theme.PaletteLight, {
}; true => theme.PaletteDark,
false => theme.PaletteLight,
};
public static string GetActivityIndicatorColor(this MudTheme theme, SettingsManager settingsManager) => settingsManager.IsDarkMode switch public string GetActivityIndicatorColor(SettingsManager settingsManager) => settingsManager.IsDarkMode switch
{ {
true => theme.GetActivityIndicatorDarkColor(), true => theme.GetActivityIndicatorDarkColor(),
false => theme.GetActivityIndicatorLightColor(), false => theme.GetActivityIndicatorLightColor(),
}; };
public static string GetActivityIndicatorLightColor(this MudTheme theme) => theme.PaletteLight.Info.Value; public string GetActivityIndicatorLightColor() => theme.PaletteLight.Info.Value;
public static string GetActivityIndicatorDarkColor(this MudTheme theme) => theme.PaletteDark.InfoLighten; public string GetActivityIndicatorDarkColor() => theme.PaletteDark.InfoLighten;
}
} }
@@ -4,63 +4,66 @@ public static class AssistantComponentTypeExtensions
{ {
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantComponentTypeExtensions).Namespace, nameof(AssistantComponentTypeExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantComponentTypeExtensions).Namespace, nameof(AssistantComponentTypeExtensions));
public static string GetDisplayName(this AssistantComponentType type) => type switch extension(AssistantComponentType type)
{ {
AssistantComponentType.FORM => TB("Root"), public string GetDisplayName() => type switch
AssistantComponentType.TEXT_AREA => TB("Text Area"), {
AssistantComponentType.BUTTON => TB("Button"), AssistantComponentType.FORM => TB("Root"),
AssistantComponentType.BUTTON_GROUP => TB("Button group"), AssistantComponentType.TEXT_AREA => TB("Text Area"),
AssistantComponentType.DROPDOWN => TB("Dropdown"), AssistantComponentType.BUTTON => TB("Button"),
AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"), AssistantComponentType.BUTTON_GROUP => TB("Button group"),
AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"), AssistantComponentType.DROPDOWN => TB("Dropdown"),
AssistantComponentType.SWITCH => TB("Switch"), AssistantComponentType.PROVIDER_SELECTION => TB("Provider Selection"),
AssistantComponentType.HEADING => TB("Heading"), AssistantComponentType.PROFILE_SELECTION => TB("Profile Selection"),
AssistantComponentType.TEXT => TB("Text"), AssistantComponentType.SWITCH => TB("Switch"),
AssistantComponentType.LIST => TB("List"), AssistantComponentType.HEADING => TB("Heading"),
AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"), AssistantComponentType.TEXT => TB("Text"),
AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"), AssistantComponentType.LIST => TB("List"),
AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"), AssistantComponentType.WEB_CONTENT_READER => TB("Web Content Reader"),
AssistantComponentType.IMAGE => TB("Image"), AssistantComponentType.FILE_CONTENT_READER => TB("File Content Reader"),
AssistantComponentType.COLOR_PICKER => TB("Color Selection"), AssistantComponentType.FILE_ATTACHMENTS => TB("File Attachments"),
AssistantComponentType.DATE_PICKER => TB("Date Selection"), AssistantComponentType.IMAGE => TB("Image"),
AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"), AssistantComponentType.COLOR_PICKER => TB("Color Selection"),
AssistantComponentType.TIME_PICKER => TB("Time Selection"), AssistantComponentType.DATE_PICKER => TB("Date Selection"),
AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"), AssistantComponentType.DATE_RANGE_PICKER => TB("Date Range Selection"),
AssistantComponentType.LAYOUT_GRID => TB("Grid"), AssistantComponentType.TIME_PICKER => TB("Time Selection"),
AssistantComponentType.LAYOUT_PAPER => TB("Container"), AssistantComponentType.LAYOUT_ITEM => TB("Grid Item"),
AssistantComponentType.LAYOUT_STACK => TB("Stack"), AssistantComponentType.LAYOUT_GRID => TB("Grid"),
AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"), AssistantComponentType.LAYOUT_PAPER => TB("Container"),
AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"), AssistantComponentType.LAYOUT_STACK => TB("Stack"),
_ => TB("Unknown Element") AssistantComponentType.LAYOUT_ACCORDION => TB("Accordion"),
}; AssistantComponentType.LAYOUT_ACCORDION_SECTION => TB("Accordion Section"),
_ => TB("Unknown Element")
};
public static string GetIcon(this AssistantComponentType type) => type switch public string GetIcon() => type switch
{ {
AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick, AssistantComponentType.BUTTON => MudBlazor.Icons.Material.Filled.AdsClick,
AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale, AssistantComponentType.BUTTON_GROUP => MudBlazor.Icons.Material.Filled.LinearScale,
AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule, AssistantComponentType.DROPDOWN => MudBlazor.Icons.Material.Filled.Rule,
AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory, AssistantComponentType.PROVIDER_SELECTION => MudBlazor.Icons.Material.Filled.Memory,
AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge, AssistantComponentType.PROFILE_SELECTION => MudBlazor.Icons.Material.Filled.Badge,
AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn, AssistantComponentType.SWITCH => MudBlazor.Icons.Material.Filled.ToggleOn,
AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title, AssistantComponentType.HEADING => MudBlazor.Icons.Material.Filled.Title,
AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields, AssistantComponentType.TEXT => MudBlazor.Icons.Material.Filled.TextFields,
AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg, AssistantComponentType.TEXT_AREA => MudBlazor.Icons.Material.Filled.Wysiwyg,
AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List, AssistantComponentType.LIST => MudBlazor.Icons.Material.Filled.List,
AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public, AssistantComponentType.WEB_CONTENT_READER => MudBlazor.Icons.Material.Filled.Public,
AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile, AssistantComponentType.FILE_CONTENT_READER => MudBlazor.Icons.Material.Filled.AttachFile,
AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile, AssistantComponentType.FILE_ATTACHMENTS => MudBlazor.Icons.Material.Filled.AttachFile,
AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image, AssistantComponentType.IMAGE => MudBlazor.Icons.Material.Filled.Image,
AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette, AssistantComponentType.COLOR_PICKER => MudBlazor.Icons.Material.Filled.Palette,
AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth, AssistantComponentType.DATE_PICKER => MudBlazor.Icons.Material.Filled.CalendarMonth,
AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange, AssistantComponentType.DATE_RANGE_PICKER => MudBlazor.Icons.Material.Filled.DateRange,
AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule, AssistantComponentType.TIME_PICKER => MudBlazor.Icons.Material.Filled.Schedule,
AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize, AssistantComponentType.LAYOUT_ITEM => MudBlazor.Icons.Material.Filled.DashboardCustomize,
AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView, AssistantComponentType.LAYOUT_GRID => MudBlazor.Icons.Material.Filled.GridView,
AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox, AssistantComponentType.LAYOUT_PAPER => MudBlazor.Icons.Material.Filled.Inbox,
AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers, AssistantComponentType.LAYOUT_STACK => MudBlazor.Icons.Material.Filled.Layers,
AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay, AssistantComponentType.LAYOUT_ACCORDION => MudBlazor.Icons.Material.Filled.CalendarViewDay,
AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit, AssistantComponentType.LAYOUT_ACCORDION_SECTION => MudBlazor.Icons.Material.Filled.HorizontalSplit,
AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree, AssistantComponentType.FORM => MudBlazor.Icons.Material.Filled.AccountTree,
_ => MudBlazor.Icons.Material.Filled.AccountTree, _ => MudBlazor.Icons.Material.Filled.AccountTree,
}; };
}
} }
@@ -6,31 +6,34 @@ public static class ILangExtensions
{ {
private static readonly ILogger<ILang> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ILang>(); private static readonly ILogger<ILang> LOGGER = Program.LOGGER_FACTORY.CreateLogger<ILang>();
public static string GetText(this ILang lang, ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null) extension(ILang lang)
{ {
var type = lang.GetType(); public string GetText(ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null)
typeName ??= type.Name;
typeNamespace ??= type.Namespace!;
// When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it:
if(typeName.Contains('`'))
typeName = typeName[..typeName.IndexOf('`')];
var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::");
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
if(plugin is NoPluginLanguage)
return fallbackEN;
if(plugin.TryGetText(key, out var text, logWarning: false))
{ {
if(string.IsNullOrWhiteSpace(text)) var type = lang.GetType();
typeName ??= type.Name;
typeNamespace ??= type.Namespace!;
// When the type's name ends with `1 or `2, etc. (i.e., generic classes), remove it:
if(typeName.Contains('`'))
typeName = typeName[..typeName.IndexOf('`')];
var ns = $"{typeNamespace}::{typeName}".ToUpperInvariant().Replace(".", "::");
var key = $"root::{ns}::T{fallbackEN.ToFNV32()}";
if(plugin is NoPluginLanguage)
return fallbackEN; return fallbackEN;
return text; if(plugin.TryGetText(key, out var text, logWarning: false))
} {
if(string.IsNullOrWhiteSpace(text))
return fallbackEN;
LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'."); return text;
return fallbackEN; }
LOGGER.LogDebug($"Missing translation key '{key}' for content '{fallbackEN}'.");
return fallbackEN;
}
} }
} }
@@ -2,11 +2,14 @@ namespace AIStudio.Tools.PluginSystem;
public static class InternalPluginExtensions public static class InternalPluginExtensions
{ {
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch extension(InternalPlugin plugin)
{ {
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"), public InternalPluginData MetaData() => plugin switch
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"), {
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"),
InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
_ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown") _ => new InternalPluginData(PluginType.NONE, Guid.Empty, "unknown")
}; };
}
} }
Loaded 100 of 138 files, more files were not shown because too many files have changed in this diff. Show more