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
+1859 -929

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,7 +6,9 @@ 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)
{
public string GetName() => level switch
{ {
AssistantAuditLevel.DANGEROUS => TB("Dangerous"), AssistantAuditLevel.DANGEROUS => TB("Dangerous"),
AssistantAuditLevel.CAUTION => TB("Concerning"), AssistantAuditLevel.CAUTION => TB("Concerning"),
@@ -14,7 +16,7 @@ public static class AssistantAuditLevelExtensions
_ => TB("Unknown"), _ => 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,
@@ -22,7 +24,7 @@ public static class AssistantAuditLevelExtensions
_ => 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,
@@ -30,13 +32,14 @@ public static class AssistantAuditLevelExtensions
_ => 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,7 +4,9 @@ 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)
{
public string Name() => numberParticipants switch
{ {
NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"), NumberParticipants.NOT_SPECIFIED => TB("Please select how many participants are expected"),
@@ -24,3 +26,4 @@ public static class NumberParticipantsExtensions
_ => "Unknown" _ => "Unknown"
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => separator switch
{ {
BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"), BatchProcessingCsvSeparator.COMMA => TB("Comma (,)"),
BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"), BatchProcessingCsvSeparator.SEMICOLON => TB("Semicolon (;)"),
@@ -17,7 +19,7 @@ public static class BatchProcessingCsvSeparatorExtensions
_ => 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 => ';',
@@ -27,6 +29,7 @@ public static class BatchProcessingCsvSeparatorExtensions
_ => DEFAULT_SEPARATOR, _ => DEFAULT_SEPARATOR,
}; };
}
internal static bool IsValidCustomSeparator(string separator) internal static bool IsValidCustomSeparator(string separator)
{ {
@@ -4,7 +4,9 @@ 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)
{
public string Name() => outputMode switch
{ {
BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"), BatchProcessingOutputMode.INDIVIDUAL_FILES => TB("One file per document"),
BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"), BatchProcessingOutputMode.TABLE_ONLY => TB("One CSV results table, where each answer becomes one row"),
@@ -12,3 +14,4 @@ public static class BatchProcessingOutputModeExtensions
_ => TB("Unknown output mode"), _ => TB("Unknown output mode"),
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => promptSource switch
{ {
BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"), BatchProcessingPromptSource.FREE_PROMPT => TB("Use a free prompt"),
BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"), BatchProcessingPromptSource.POLICY => TB("Use a document analysis policy"),
@@ -13,3 +15,4 @@ public static class BatchProcessingPromptSourceExtensions
_ => TB("Unknown prompt source"), _ => TB("Unknown prompt source"),
}; };
} }
}
@@ -2,9 +2,11 @@ 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)
{
public string GetMessage(string technicalDetails) => parseError switch
{ {
LuaResponseParseError.MISSING_JSON_OBJECT => TB("The model response is missing or unreadable."), LuaResponseParseError.MISSING_JSON_OBJECT => TB("The model response is missing or unreadable."),
LuaResponseParseError.INVALID_JSON => string.IsNullOrWhiteSpace(technicalDetails) LuaResponseParseError.INVALID_JSON => string.IsNullOrWhiteSpace(technicalDetails)
@@ -21,3 +23,4 @@ public static class LuaResponseParseErrorExtension
_ => TB("The model returned an unusable JSON response."), _ => TB("The model returned an unusable JSON response."),
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => language switch
{ {
CommonCodingLanguages.NONE => TB("None"), CommonCodingLanguages.NONE => TB("None"),
@@ -43,3 +45,4 @@ public static class CommonCodingLanguageExtensions
_ => TB("Unknown") _ => TB("Unknown")
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => style switch
{ {
WritingStyles.ACADEMIC => TB("Academic"), WritingStyles.ACADEMIC => TB("Academic"),
WritingStyles.PERSONAL => TB("Personal"), WritingStyles.PERSONAL => TB("Personal"),
@@ -14,7 +16,7 @@ public static class WritingStylesExtensions
_ => 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.",
@@ -24,3 +26,4 @@ public static class WritingStylesExtensions
_ => "Use a formal business style for this e-mail.", _ => "Use a formal business style for this e-mail.",
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Description() => provider switch
{ {
AllowedLLMProviders.NONE => TB("Please select what kind of LLM provider are allowed for this data source"), 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.ANY => TB("Any LLM provider is allowed: users might choose a cloud-based or a self-hosted provider"),
@@ -13,3 +15,4 @@ public static class AllowedLLMProvidersExtensions
_ => TB("Unknown option was selected") _ => TB("Unknown option was selected")
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => auth switch
{ {
Auth.NONE => TB("No login necessary: useful for public data sources"), Auth.NONE => TB("No login necessary: useful for public data sources"),
@@ -15,7 +17,7 @@ public static class AuthExtensions
_ => 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.",
@@ -26,3 +28,4 @@ public static class AuthExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => dataSource switch
{ {
DataSources.NONE => TB("No data source selected"), DataSources.NONE => TB("No data source selected"),
DataSources.CUSTOM => TB("Custom description"), DataSources.CUSTOM => TB("Custom description"),
@@ -19,3 +21,4 @@ public static class DataSourcesExtensions
_ => 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));
/// <param name="outcome">What became of the file.</param>
extension(ERIServerFileOutcome outcome)
{
/// <summary> /// <summary>
/// Gets the localized message which explains why a file was not written. /// Gets the localized message which explains why a file was not written.
/// </summary> /// </summary>
/// <param name="outcome">What became of the file.</param>
/// <param name="file">The path of the file, as the model named it.</param> /// <param name="file">The path of the file, as the model named it.</param>
/// <returns>The localized message.</returns> /// <returns>The localized message.</returns>
internal static string ToUserMessage(this ERIServerFileOutcome outcome, string file) => string.Format(ToUserMessageFormat(outcome), file); 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 Task<string> ReadSpecification(WebRootFiles webRootFiles) => webRootFiles.ReadTextAsync(version.SpecificationURL());
public static string SpecificationURL(this ERIVersion version) public string SpecificationURL()
{ {
var nameLower = version.ToString().ToLowerInvariant(); var nameLower = version.ToString().ToLowerInvariant();
var filename = $"{nameLower}.json"; var filename = $"{nameLower}.json";
return $"specs/eri/{filename}"; return $"specs/eri/{filename}";
} }
public static bool WasSpecificationSelected(this ERIVersion version) => version != ERIVersion.NONE; public bool WasSpecificationSelected() => version != ERIVersion.NONE;
}
} }
@@ -4,7 +4,9 @@ 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)
{
public string Name() => os switch
{ {
OperatingSystem.NONE => TB("No operating system specified"), OperatingSystem.NONE => TB("No operating system specified"),
@@ -14,7 +16,7 @@ public static class OperatingSystemExtensions
_ => 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",
@@ -24,3 +26,4 @@ public static class OperatingSystemExtensions
_ => "Unknown operating system" _ => "Unknown operating system"
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => language switch
{ {
ProgrammingLanguages.NONE => TB("No programming language selected"), ProgrammingLanguages.NONE => TB("No programming language selected"),
@@ -24,7 +26,7 @@ public static class ProgrammingLanguagesExtensions
_ => 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",
@@ -44,3 +46,4 @@ public static class ProgrammingLanguagesExtensions
_ => "Unknown" _ => "Unknown"
}; };
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Assistants.IconFinder;
public static class IconSourceExtensions public static class IconSourceExtensions
{ {
public static string Name(this IconSources iconSource) => iconSource switch extension(IconSources iconSource)
{
public string Name() => iconSource switch
{ {
IconSources.FLAT_ICON => "Flaticon", IconSources.FLAT_ICON => "Flaticon",
IconSources.FONT_AWESOME => "Font Awesome", IconSources.FONT_AWESOME => "Font Awesome",
@@ -14,7 +16,7 @@ public static class IconSourceExtensions
_ => "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`.",
@@ -26,7 +28,7 @@ public static class IconSourceExtensions
_ => 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/",
@@ -38,3 +40,4 @@ public static class IconSourceExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => sentenceStructure switch
{ {
SentenceStructure.ACTIVE => TB("Active voice"), SentenceStructure.ACTIVE => TB("Active voice"),
SentenceStructure.PASSIVE => TB("Passive voice"), SentenceStructure.PASSIVE => TB("Passive voice"),
@@ -12,7 +14,7 @@ public static class SentenceStructureExtensions
_ => 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.",
@@ -20,3 +22,4 @@ public static class SentenceStructureExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => style switch
{ {
WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"), WritingStyles.EVERYDAY => TB("Everyday (personal texts, social media)"),
WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"), WritingStyles.BUSINESS => TB("Business (business emails, reports, presentations)"),
@@ -20,7 +22,7 @@ public static class WritingStylesExtensions
_ => 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.",
@@ -36,3 +38,4 @@ public static class WritingStylesExtensions
_ => "Keep the style of the text as it is.", _ => "Keep the style of the text as it is.",
}; };
} }
}
@@ -1,11 +1,13 @@
namespace AIStudio.Assistants.SWOTAnalysis; namespace AIStudio.Assistants.SWOTAnalysis;
internal static class SwotAnalysisCategoryExtensions internal static class SwotAnalysisCategoryExtensions
{
extension(SwotAnalysisCategory category)
{ {
/// <summary> /// <summary>
/// The name of the category in the JSON the models read and write, e.g., "strengths". /// The name of the category in the JSON the models read and write, e.g., "strengths".
/// </summary> /// </summary>
public static string JsonName(this SwotAnalysisCategory category) => category switch public string JsonName() => category switch
{ {
SwotAnalysisCategory.STRENGTHS => "strengths", SwotAnalysisCategory.STRENGTHS => "strengths",
SwotAnalysisCategory.WEAKNESSES => "weaknesses", SwotAnalysisCategory.WEAKNESSES => "weaknesses",
@@ -18,7 +20,7 @@ internal static class SwotAnalysisCategoryExtensions
/// <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",
@@ -28,3 +30,4 @@ internal static class SwotAnalysisCategoryExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => ageGroup switch
{ {
AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"), AudienceAgeGroup.UNSPECIFIED => TB("Unspecified age group"),
AudienceAgeGroup.CHILDREN => TB("Children"), AudienceAgeGroup.CHILDREN => TB("Children"),
@@ -14,7 +16,7 @@ public static class AudienceAgeGroupExtensions
_ => 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.",
@@ -24,3 +26,4 @@ public static class AudienceAgeGroupExtensions
_ => "Do not tailor the text to a specific age group.", _ => "Do not tailor the text to a specific age group.",
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => expertise switch
{ {
AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"), AudienceExpertise.UNSPECIFIED => TB("Unspecified expertise"),
AudienceExpertise.NON_EXPERTS => TB("No expertise"), AudienceExpertise.NON_EXPERTS => TB("No expertise"),
@@ -15,7 +17,7 @@ public static class AudienceExpertiseExtensions
_ => 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.",
@@ -26,3 +28,4 @@ public static class AudienceExpertiseExtensions
_ => "Do not tailor the text to a specific expertise level.", _ => "Do not tailor the text to a specific expertise level.",
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => level switch
{ {
AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"), AudienceOrganizationalLevel.UNSPECIFIED => TB("Unspecified organizational level"),
AudienceOrganizationalLevel.TRAINEES => TB("Trainees"), AudienceOrganizationalLevel.TRAINEES => TB("Trainees"),
@@ -17,7 +19,7 @@ public static class AudienceOrganizationalLevelExtensions
_ => 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.",
@@ -30,3 +32,4 @@ public static class AudienceOrganizationalLevelExtensions
_ => "Do not tailor the text to a specific organizational level.", _ => "Do not tailor the text to a specific organizational level.",
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => profile switch
{ {
AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"), AudienceProfile.UNSPECIFIED => TB("Unspecified audience profile"),
AudienceProfile.STUDENTS => TB("Students"), AudienceProfile.STUDENTS => TB("Students"),
@@ -21,7 +23,7 @@ public static class AudienceProfileExtensions
_ => 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.",
@@ -38,3 +40,4 @@ public static class AudienceProfileExtensions
_ => "Do not tailor the text to a specific audience profile.", _ => "Do not tailor the text to a specific audience profile.",
}; };
} }
}
@@ -4,7 +4,10 @@ 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 /// <param name="occasion">The occasion.</param>
extension(PresentationOccasion occasion)
{
public string Name() => occasion switch
{ {
PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"), PresentationOccasion.UNSPECIFIED => TB("Unspecified occasion"),
PresentationOccasion.TALK => TB("Talk"), PresentationOccasion.TALK => TB("Talk"),
@@ -30,9 +33,8 @@ public static class PresentationOccasionExtensions
/// <summary> /// <summary>
/// Tells the model what the occasion means for the structure of the presentation. /// Tells the model what the occasion means for the structure of the presentation.
/// </summary> /// </summary>
/// <param name="occasion">The occasion.</param>
/// <param name="customOccasion">The occasion the user described, for OTHER.</param> /// <param name="customOccasion">The occasion the user described, for OTHER.</param>
public static string Prompt(this PresentationOccasion occasion, string customOccasion) => occasion switch 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.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.KEYNOTE => "The presentation accompanies a keynote. Favor few words per slide, strong key messages, and a story which carries the audience.",
@@ -54,3 +56,4 @@ public static class PresentationOccasionExtensions
_ => "No particular occasion is given. Structure the presentation by its content.", _ => "No particular occasion is given. Structure the presentation by its content.",
}; };
} }
}
@@ -11,6 +11,8 @@ internal static class TemplateSlideExtensions
/// </summary> /// </summary>
private const int MAX_SLIDE_TITLE_CHARACTERS = 60; private const int MAX_SLIDE_TITLE_CHARACTERS = 60;
extension(TemplateSlide slide)
{
/// <summary> /// <summary>
/// How the forms name a slide of the template: by its number and its title, and what is special about it. /// How the forms name a slide of the template: by its number and its title, and what is special about it.
/// </summary> /// </summary>
@@ -18,7 +20,7 @@ internal static class TemplateSlideExtensions
/// The title tells the slides apart at a glance, since a template rarely gives its slides more /// 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. /// than a title. A long one is cut, so that the label stays on one line.
/// </remarks> /// </remarks>
public static string Label(this TemplateSlide slide) public string Label()
{ {
var title = slide.Title.Length > MAX_SLIDE_TITLE_CHARACTERS ? $"{slide.Title[..MAX_SLIDE_TITLE_CHARACTERS].TrimEnd()}…" : slide.Title; 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); var label = title.Length > 0 ? string.Format(TB("Slide {0}: {1}"), slide.Number, title) : string.Format(TB("Slide {0}"), slide.Number);
@@ -28,3 +30,4 @@ internal static class TemplateSlideExtensions
return label; return label;
} }
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => use switch
{ {
TemplateSlideUse.OMIT => TB("Leave out"), TemplateSlideUse.OMIT => TB("Leave out"),
TemplateSlideUse.KEEP => TB("Keep unchanged"), TemplateSlideUse.KEEP => TB("Keep unchanged"),
@@ -15,3 +17,4 @@ internal static class TemplateSlideUseExtensions
_ => TB("Leave out"), _ => TB("Leave out"),
}; };
} }
}
@@ -4,7 +4,9 @@ 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)
{
public string Name() => complexity switch
{ {
Complexity.NO_CHANGE => TB("No change in complexity"), Complexity.NO_CHANGE => TB("No change in complexity"),
@@ -18,7 +20,7 @@ public static class ComplexityExtensions
_ => 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.",
@@ -32,3 +34,4 @@ public static class ComplexityExtensions
_ => "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));
/// <param name="failure">The recorded failure.</param>
extension(VisualBriefingFailure failure)
{
/// <summary> /// <summary>
/// Gets the localized message for one recorded failure. /// Gets the localized message for one recorded failure.
/// </summary> /// </summary>
/// <param name="failure">The recorded failure.</param>
/// <returns>The localized message.</returns> /// <returns>The localized message.</returns>
internal static string ToUserMessage(this VisualBriefingFailure failure) => ToUserMessage(failure.Code, failure.ValidationRule); 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,12 +43,14 @@ internal static class VisualBriefingFailureExtensions
_ => code.ToUserMessage(), _ => code.ToUserMessage(),
}; };
/// <param name="rule">The stable validation rule.</param>
extension(VisualBriefingValidationRule rule)
{
/// <summary> /// <summary>
/// Gets the localized message for one validation rule. /// Gets the localized message for one validation rule.
/// </summary> /// </summary>
/// <param name="rule">The stable validation rule.</param>
/// <returns>The localized message.</returns> /// <returns>The localized message.</returns>
private static string ToUserMessage(this VisualBriefingValidationRule rule) => rule switch private string ToUserMessage() => rule switch
{ {
VisualBriefingValidationRule.JSON_INVALID => TB("The model did not return valid JSON. 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.VALUE_TYPE_INVALID => TB("The model response contained a value of the wrong type. 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."),
@@ -74,13 +79,16 @@ internal static class VisualBriefingFailureExtensions
_ => string.Empty, _ => string.Empty,
}; };
}
/// <param name="code">The stable failure code.</param>
extension(VisualBriefingFailureCode code)
{
/// <summary> /// <summary>
/// Gets the localized message for one failure code. /// Gets the localized message for one failure code.
/// </summary> /// </summary>
/// <param name="code">The stable failure code.</param>
/// <returns>The localized message.</returns> /// <returns>The localized message.</returns>
private static string ToUserMessage(this VisualBriefingFailureCode code) => code switch private string ToUserMessage() => code switch
{ {
VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."), VisualBriefingFailureCode.PROVIDER_NOT_SELECTED => TB("This briefing has no provider selected. Please select a provider before you generate a briefing."),
VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."), VisualBriefingFailureCode.PROVIDER_NOT_ALLOWED => TB("The selected provider may not see the content of this briefing. Please select another provider."),
@@ -107,3 +115,4 @@ internal static class VisualBriefingFailureExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -15,11 +15,13 @@ 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}";
/// <param name="app">The web application.</param>
extension(WebApplication app)
{
/// <summary> /// <summary>
/// Maps the visual briefing preview endpoint. /// Maps the visual briefing preview endpoint.
/// </summary> /// </summary>
/// <param name="app">The web application.</param> public void MapVisualBriefingPreview() => app.MapGet(
public static void MapVisualBriefingPreview(this WebApplication app) => app.MapGet(
ROUTE, ROUTE,
async ( async (
Guid briefingId, Guid briefingId,
@@ -64,6 +66,7 @@ internal static class VisualBriefingPreviewEndpoint
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.
@@ -16,6 +16,9 @@ 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));
/// <param name="thread">The chat.</param>
extension(ChatThread thread)
{
/// <summary> /// <summary>
/// Collects the entries of the provenance store which a chat refers to. /// Collects the entries of the provenance store which a chat refers to.
/// </summary> /// </summary>
@@ -24,9 +27,8 @@ public static class ChatProvenance
/// the message being written, which may come from a message sent before, e.g., one being edited, /// 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. /// together with the texts a preview read of them.
/// </remarks> /// </remarks>
/// <param name="thread">The chat.</param>
/// <returns>The ids of the entries.</returns> /// <returns>The ids of the entries.</returns>
public static IReadOnlySet<Guid> CollectProvenanceIds(this ChatThread thread) public IReadOnlySet<Guid> CollectProvenanceIds()
{ {
var provenanceIds = new HashSet<Guid>(); var provenanceIds = new HashSet<Guid>();
foreach (var block in thread.Blocks) foreach (var block in thread.Blocks)
@@ -42,6 +44,7 @@ public static class ChatProvenance
provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId)); provenanceIds.UnionWith(thread.Draft.PreviewedTexts.Select(text => text.ProvenanceId));
return provenanceIds; return provenanceIds;
} }
}
/// <summary> /// <summary>
/// Deletes what the given chats received from the provenance store, for chats which were deleted. /// Deletes what the given chats received from the provenance store, for chats which were deleted.
@@ -5,6 +5,9 @@ using AIStudio.Settings.DataModel;
namespace AIStudio.Chat; namespace AIStudio.Chat;
public static class ChatRestrictionsExtensions public static class ChatRestrictionsExtensions
{
/// <param name="restrictions">What the content demands.</param>
extension(ChatRestrictions restrictions)
{ {
/// <summary> /// <summary>
/// Checks if the specified provider may see content with these restrictions. /// Checks if the specified provider may see content with these restrictions.
@@ -16,10 +19,9 @@ public static class ChatRestrictionsExtensions
/// That kind of check is done when the available data sources are resolved. What counts here is /// 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. /// what the content demands since it holds data of them, see ChatThreadExtensions.IsLLMProviderAllowed.
/// </remarks> /// </remarks>
/// <param name="restrictions">What the content demands.</param>
/// <param name="provider">The provider to check.</param> /// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider may see the content. False, otherwise.</returns> /// <returns>True, when the provider may see the content. False, otherwise.</returns>
public static bool IsLLMProviderAllowed<T>(this ChatRestrictions restrictions, T provider) public bool IsLLMProviderAllowed<T>(T provider)
{ {
var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>(); var settingsManager = Program.SERVICE_PROVIDER.GetRequiredService<SettingsManager>();
@@ -73,6 +75,7 @@ public static class ChatRestrictionsExtensions
false => restrictions.DataSecurity is not DataSourceSecurity.SELF_HOSTED, 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,12 +6,14 @@ 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));
/// <param name="role">The role.</param>
extension(ChatRole role)
{
/// <summary> /// <summary>
/// Returns the name of the role. /// Returns the name of the role.
/// </summary> /// </summary>
/// <param name="role">The role.</param>
/// <returns>The name of the role.</returns> /// <returns>The name of the role.</returns>
public static string ToName(this ChatRole role) => role switch public string ToName() => role switch
{ {
ChatRole.SYSTEM => TB("System"), ChatRole.SYSTEM => TB("System"),
ChatRole.USER => TB("You"), ChatRole.USER => TB("You"),
@@ -23,9 +25,8 @@ public static class ChatRoleExtensions
/// <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 static Color ToColor(this ChatRole role) => role switch public Color ToColor() => role switch
{ {
ChatRole.SYSTEM => Color.Info, ChatRole.SYSTEM => Color.Info,
ChatRole.USER => Color.Primary, ChatRole.USER => Color.Primary,
@@ -37,9 +38,8 @@ public static class ChatRoleExtensions
/// <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 static string ToIcon(this ChatRole role) => role switch public string ToIcon() => 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,
@@ -51,9 +51,8 @@ public static class ChatRoleExtensions
/// <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 static string ToChatTemplateName(this ChatRole role) => role switch public string ToChatTemplateName() => role switch
{ {
ChatRole.SYSTEM => TB("System"), ChatRole.SYSTEM => TB("System"),
ChatRole.USER => TB("User"), ChatRole.USER => TB("User"),
@@ -65,11 +64,11 @@ public static class ChatRoleExtensions
/// <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 static ChatRole SelectNextRoleForTemplate(this ChatRole currentRole) => currentRole switch public ChatRole SelectNextRoleForTemplate() => role switch
{ {
ChatRole.USER => ChatRole.AI, ChatRole.USER => ChatRole.AI,
_ => ChatRole.USER, _ => ChatRole.USER,
}; };
} }
}
@@ -1,6 +1,9 @@
namespace AIStudio.Chat; namespace AIStudio.Chat;
public static class ChatThreadExtensions public static class ChatThreadExtensions
{
/// <param name="chatThread">The chat thread to check; null for a new chat, which demands nothing yet.</param>
extension(ChatThread? chatThread)
{ {
/// <summary> /// <summary>
/// Checks if the specified provider is allowed for the chat thread. /// Checks if the specified provider is allowed for the chat thread.
@@ -18,8 +21,8 @@ public static class ChatThreadExtensions
/// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left /// The check itself is ChatRestrictionsExtensions.IsLLMProviderAllowed, which content that left
/// the thread is checked with as well. /// the thread is checked with as well.
/// </remarks> /// </remarks>
/// <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> /// <param name="provider">The provider to check.</param>
/// <returns>True, when the provider is allowed for the chat thread. False, otherwise.</returns> /// <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); public bool IsLLMProviderAllowed<T>(T provider) => (chatThread?.CaptureRestrictions() ?? ChatRestrictions.NONE).IsLLMProviderAllowed(provider);
}
} }
@@ -1,6 +1,9 @@
namespace AIStudio.Chat; namespace AIStudio.Chat;
public static class IContentExtensions public static class IContentExtensions
{
/// <param name="content">The content of a message in the chat.</param>
extension(IContent content)
{ {
/// <summary> /// <summary>
/// Detaches whoever listens to the stream of this content. /// Detaches whoever listens to the stream of this content.
@@ -11,8 +14,7 @@ public static class IContentExtensions
/// so handlers left behind would keep those components alive for as long as the thread exists. /// 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. /// Whoever registers a handler calls this when it is no longer needed.
/// </remarks> /// </remarks>
/// <param name="content">The content whose streaming handlers you want to detach.</param> public void ResetStreamingHandlers()
public static void ResetStreamingHandlers(this IContent content)
{ {
content.StreamingEvent = IContent.NO_STREAMING_HANDLER; content.StreamingEvent = IContent.NO_STREAMING_HANDLER;
content.StreamingDone = IContent.NO_STREAMING_HANDLER; content.StreamingDone = IContent.NO_STREAMING_HANDLER;
@@ -28,10 +30,9 @@ public static class IContentExtensions
/// the model wrote and nothing else: whoever reads a table out of a message wants exactly that, /// 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. /// while whoever writes a file wants the sources along with it and asks for the export reading.
/// </remarks> /// </remarks>
/// <param name="content">The content to read.</param>
/// <param name="markdown">The Markdown text, or an empty string when there is none.</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> /// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetMarkdownText(this IContent content, out string markdown) public bool TryGetMarkdownText(out string markdown)
{ {
if (content is ContentText text) if (content is ContentText text)
{ {
@@ -53,13 +54,12 @@ public static class IContentExtensions
/// sources are not part of the text the model wrote, they hang on the content, which is why /// 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. /// every path out of the app asks for this and not for the text alone.
/// </remarks> /// </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="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 /// <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 whose reader stumbles over such a link says no here; the clipboard and every text
/// format keep the page.</param> /// format keep the page.</param>
/// <returns>True, when this content carries Markdown text.</returns> /// <returns>True, when this content carries Markdown text.</returns>
public static bool TryGetExportMarkdown(this IContent content, out string markdown, bool keepPageAnchors = true) public bool TryGetExportMarkdown(out string markdown, bool keepPageAnchors = true)
{ {
if (content is not ContentText text) if (content is not ContentText text)
{ {
@@ -91,10 +91,9 @@ public static class IContentExtensions
/// is what the user chose to do; the chat still never renders it. Code only gets what its tools /// 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. /// expect of a file, see ToSourceCodeFile.
/// </remarks> /// </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> /// <param name="file">The file, as PlainFileExport.ExtractFiles read it out of this content.</param>
/// <returns>The content of the file to write.</returns> /// <returns>The content of the file to write.</returns>
public static string ToExportContent(this IContent content, MessageFile file) public string ToExportContent(MessageFile file)
{ {
if (file.Format.IsTabular()) if (file.Format.IsTabular())
return file.Content; return file.Content;
@@ -111,6 +110,7 @@ public static class IContentExtensions
return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}"; return $"{file.Content}{Environment.NewLine}{Environment.NewLine}{comment}";
} }
}
/// <summary> /// <summary>
/// Turns a code block into the file its tools expect. /// Turns a code block into the file its tools expect.
@@ -30,6 +30,9 @@ 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);
} }
/// <param name="image">The image source.</param>
extension(IImageSource image)
{
/// <summary> /// <summary>
/// Read the image content as a base64 string. /// Read the image content as a base64 string.
/// </summary> /// </summary>
@@ -46,10 +49,9 @@ public static class IImageSourceExtensions
/// any data URI prefix (like "data:image/png;base64,"). The caller has /// any data URI prefix (like "data:image/png;base64,"). The caller has
/// to take care of that if needed. /// to take care of that if needed.
/// </remarks> /// </remarks>
/// <param name="image">The image source.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>The image content as a base64 string; might be empty.</returns> /// <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) public async Task<(bool success, string base64Content)> TryAsBase64(CancellationToken token = default)
{ {
switch (image.SourceType) switch (image.SourceType)
{ {
@@ -103,3 +105,4 @@ public static class IImageSourceExtensions
} }
} }
} }
}
@@ -8,19 +8,20 @@ using AIStudio.Tools.Services;
namespace AIStudio.Chat; namespace AIStudio.Chat;
public static class ListContentBlockExtensions public static class ListContentBlockExtensions
{
/// <param name="blocks">The list of content blocks to process.</param>
extension(List<ContentBlock> blocks)
{ {
/// <summary> /// <summary>
/// Processes a list of content blocks by transforming them into a collection of message results asynchronously. /// Processes a list of content blocks by transforming them into a collection of message results asynchronously.
/// </summary> /// </summary>
/// <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> /// <param name="roleTransformer">A function that transforms each content block into a message result asynchronously.</param>
/// <param name="provider">The configured provider, whose model is being written to.</param> /// <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="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="imageSubContentFactory">A factory function to create image sub-content from an image prepared for the model.</param>
/// <param name="token">The cancellation token.</param> /// <param name="token">The cancellation token.</param>
/// <returns>An asynchronous task that resolves to a list of transformed results.</returns> /// <returns>An asynchronous task that resolves to a list of transformed results.</returns>
public static async Task<IList<IMessageBase>> BuildMessagesAsync( public async Task<IList<IMessageBase>> BuildMessagesAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider, AIStudio.Settings.Provider provider,
Func<ChatRole, string> roleTransformer, Func<ChatRole, string> roleTransformer,
Func<string, ISubContent> textSubContentFactory, Func<string, ISubContent> textSubContentFactory,
@@ -125,7 +126,6 @@ public static class ListContentBlockExtensions
/// <summary> /// <summary>
/// Processes a list of content blocks using direct image URL format to create message results asynchronously. /// Processes a list of content blocks using direct image URL format to create message results asynchronously.
/// </summary> /// </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> /// <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> /// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks> /// <remarks>
@@ -135,8 +135,7 @@ public static class ListContentBlockExtensions
/// </code> /// </code>
/// This format is used by OpenAI, Mistral, and Ollama. /// This format is used by OpenAI, Mistral, and Ollama.
/// </remarks> /// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync( public async Task<IList<IMessageBase>> BuildMessagesUsingDirectImageUrlAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider, provider,
StandardRoleTransformer, StandardRoleTransformer,
@@ -146,7 +145,6 @@ public static class ListContentBlockExtensions
/// <summary> /// <summary>
/// Processes a list of content blocks using nested image URL format to create message results asynchronously. /// Processes a list of content blocks using nested image URL format to create message results asynchronously.
/// </summary> /// </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> /// <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> /// <returns>An asynchronous task that resolves to a list of transformed message results.</returns>
/// <remarks> /// <remarks>
@@ -156,13 +154,13 @@ public static class ListContentBlockExtensions
/// </code> /// </code>
/// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers. /// This format is used by LM Studio, VLLM, llama.cpp, and other OpenAI-compatible providers.
/// </remarks> /// </remarks>
public static async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync( public async Task<IList<IMessageBase>> BuildMessagesUsingNestedImageUrlAsync(
this List<ContentBlock> blocks,
AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync( AIStudio.Settings.Provider provider) => await blocks.BuildMessagesAsync(
provider, provider,
StandardRoleTransformer, StandardRoleTransformer,
StandardTextSubContentFactory, StandardTextSubContentFactory,
NestedImageSubContentFactory); NestedImageSubContentFactory);
}
private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText private static ISubContent StandardTextSubContentFactory(string text) => new SubContentText
{ {
@@ -2,7 +2,10 @@ namespace AIStudio.Chat;
public static class ListFileAttachmentExtensions public static class ListFileAttachmentExtensions
{ {
public static bool ContainsImages(this List<FileAttachment> attachments) => attachments.Any(attachment => attachment.IsImage); /// <param name="attachments">The attachments of the message.</param>
extension(List<FileAttachment> attachments)
{
public bool ContainsImages() => attachments.Any(attachment => attachment.IsImage);
/// <summary> /// <summary>
/// Whether preparing the message for the AI may read a document through the runtime. /// Whether preparing the message for the AI may read a document through the runtime.
@@ -12,7 +15,7 @@ public static class ListFileAttachmentExtensions
/// before it is read, cf. FileAttachment.Normalize, and may turn out to be a document then. /// 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. /// Checking that here would touch every file twice, which takes long on a network drive.
/// </remarks> /// </remarks>
/// <param name="attachments">The attachments of the message.</param>
/// <returns>True when a document may be read.</returns> /// <returns>True when a document may be read.</returns>
public static bool MayContainDocuments(this List<FileAttachment> attachments) => attachments.Any(attachment => !attachment.IsImage); public bool MayContainDocuments() => attachments.Any(attachment => !attachment.IsImage);
}
} }
@@ -1,6 +1,9 @@
namespace AIStudio.Chat; namespace AIStudio.Chat;
public static class SetFileAttachmentExtensions public static class SetFileAttachmentExtensions
{
/// <param name="attachments">The attachments of a message.</param>
extension(HashSet<FileAttachment> attachments)
{ {
/// <summary> /// <summary>
/// Attaches a file in place of the attachment of the same file the set held before. /// Attaches a file in place of the attachment of the same file the set held before.
@@ -11,11 +14,11 @@ public static class SetFileAttachmentExtensions
/// would stay in the set, and the message would go with the old and the new text of the file. /// 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. /// Attaching a file again means the file as it is now.
/// </remarks> /// </remarks>
/// <param name="attachments">The attachments of a message.</param>
/// <param name="attachment">The attachment of the file just attached.</param> /// <param name="attachment">The attachment of the file just attached.</param>
public static void AttachInPlaceOfSameFile(this HashSet<FileAttachment> attachments, FileAttachment attachment) public void AttachInPlaceOfSameFile(FileAttachment attachment)
{ {
attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal)); attachments.RemoveWhere(existing => string.Equals(existing.FilePath, attachment.FilePath, StringComparison.Ordinal));
attachments.Add(attachment); attachments.Add(attachment);
} }
} }
}
+10 -7
View File
@@ -2,18 +2,21 @@
public static class StringExtensions public static class StringExtensions
{ {
public static string RemoveThinkTags(this string input) /// <param name="text">The text.</param>
extension(string text)
{
public string RemoveThinkTags()
{ {
const string OPEN_TAG = "<think>"; const string OPEN_TAG = "<think>";
const string CLOSE_TAG = "</think>"; const string CLOSE_TAG = "</think>";
if (string.IsNullOrWhiteSpace(input) || !input.StartsWith(OPEN_TAG, StringComparison.Ordinal)) if (string.IsNullOrWhiteSpace(text) || !text.StartsWith(OPEN_TAG, StringComparison.Ordinal))
return input; return text;
var endIndex = input.IndexOf(CLOSE_TAG, StringComparison.Ordinal); var endIndex = text.IndexOf(CLOSE_TAG, StringComparison.Ordinal);
if (endIndex == -1) if (endIndex == -1)
return string.Empty; return string.Empty;
return input[(endIndex + CLOSE_TAG.Length)..]; return text[(endIndex + CLOSE_TAG.Length)..];
} }
/// <summary> /// <summary>
@@ -24,10 +27,9 @@ public static class StringExtensions
/// Takes and returns a string rather than a span on purpose: most texts are short enough, and /// 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. /// those come back as the very same instance. Only a text which is cut costs one new string.
/// </remarks> /// </remarks>
/// <param name="text">The text.</param>
/// <param name="maxCharacters">How many characters of the text to keep at most, the dots not counted.</param> /// <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> /// <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) public string Shorten(int maxCharacters)
{ {
if (text.Length <= maxCharacters) if (text.Length <= maxCharacters)
return text; return text;
@@ -36,3 +38,4 @@ public static class StringExtensions
return string.Concat(text.AsSpan(0, end).TrimEnd(), "..."); return string.Concat(text.AsSpan(0, end).TrimEnd(), "...");
} }
} }
}
@@ -6,12 +6,14 @@ 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));
/// <param name="step">The ReadWebContentSteps enum value.</param>
extension(ReadWebContentSteps step)
{
/// <summary> /// <summary>
/// Gets the text representation of a given ReadWebContentSteps enum value. /// Gets the text representation of a given ReadWebContentSteps enum value.
/// </summary> /// </summary>
/// <param name="step">The ReadWebContentSteps enum value.</param>
/// <returns>>The text representation of the process step.</returns> /// <returns>>The text representation of the process step.</returns>
public static string GetText(this ReadWebContentSteps step) => step switch public string GetText() => step switch
{ {
ReadWebContentSteps.START => TB("Start"), ReadWebContentSteps.START => TB("Start"),
ReadWebContentSteps.LOADING => TB("Loading"), ReadWebContentSteps.LOADING => TB("Loading"),
@@ -22,3 +24,4 @@ public static class ReadWebContentStepsExtensions
_ => TB("n/a") _ => TB("n/a")
}; };
} }
}
@@ -7,7 +7,9 @@ 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)
{
public string GetName() => level switch
{ {
ConfidenceLevel.NONE => TB("No provider selected"), ConfidenceLevel.NONE => TB("No provider selected"),
@@ -22,7 +24,7 @@ public static class ConfidenceLevelExtensions
_ => 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",
@@ -52,5 +54,6 @@ public static class ConfidenceLevelExtensions
(_, 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,12 +6,14 @@ 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));
/// <param name="provider">The inference provider.</param>
extension(HFInferenceProvider provider)
{
/// <summary> /// <summary>
/// The slug Hugging Face uses for this inference provider. /// The slug Hugging Face uses for this inference provider.
/// </summary> /// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns> /// <returns>The slug, or an empty string for the routing strategies, which name no provider.</returns>
public static string EndpointsId(this HFInferenceProvider provider) => provider switch public string EndpointsId() => provider switch
{ {
HFInferenceProvider.BASETEN => "baseten", HFInferenceProvider.BASETEN => "baseten",
HFInferenceProvider.CEREBRAS => "cerebras", HFInferenceProvider.CEREBRAS => "cerebras",
@@ -39,9 +41,8 @@ public static class HFInferenceProviderExtensions
/// 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 static string ModelSuffix(this HFInferenceProvider provider) => provider switch public string ModelSuffix() => provider switch
{ {
HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty, HFInferenceProvider.NONE or HFInferenceProvider.AUTOMATIC => string.Empty,
@@ -54,9 +55,8 @@ public static class HFInferenceProviderExtensions
/// <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 static bool SupportsChat(this HFInferenceProvider provider) => provider is not HFInferenceProvider.NONE; public bool SupportsChat() => provider is not HFInferenceProvider.NONE;
/// <summary> /// <summary>
/// Whether this inference provider creates embeddings for us. /// Whether this inference provider creates embeddings for us.
@@ -67,9 +67,8 @@ public static class HFInferenceProviderExtensions
/// 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 static bool SupportsEmbeddings(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; public bool SupportsEmbeddings() => 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.
@@ -80,9 +79,8 @@ public static class HFInferenceProviderExtensions
/// 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 static bool SupportsTranscription(this HFInferenceProvider provider) => provider is HFInferenceProvider.TOGETHER_AI or HFInferenceProvider.DEEPINFRA; public bool SupportsTranscription() => 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.
@@ -93,9 +91,8 @@ public static class HFInferenceProviderExtensions
/// 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 static string ProviderBaseURL(this HFInferenceProvider provider) => provider switch public string ProviderBaseURL() => 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/",
@@ -103,36 +100,17 @@ public static class HFInferenceProviderExtensions
_ => string.Empty, _ => 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>
/// <returns>The model without its routing suffix.</returns>
public static Model WithoutRoutingSuffix(this Model model)
{
var separatorIndex = model.Id.LastIndexOf(':');
return separatorIndex is -1 ? model : model with { Id = model.Id[..separatorIndex] };
}
/// <summary> /// <summary>
/// The value to filter the Hugging Face model catalog by. /// The value to filter the Hugging Face model catalog by.
/// </summary> /// </summary>
/// <param name="provider">The inference provider.</param>
/// <returns>The provider slug, or "all" when no particular provider was chosen.</returns> /// <returns>The provider slug, or "all" when no particular provider was chosen.</returns>
public static string CatalogFilter(this HFInferenceProvider provider) public string CatalogFilter()
{ {
var slug = provider.EndpointsId(); var slug = provider.EndpointsId();
return string.IsNullOrEmpty(slug) ? "all" : slug; return string.IsNullOrEmpty(slug) ? "all" : slug;
} }
public static string ToName(this HFInferenceProvider provider) => provider switch public string ToName() => provider switch
{ {
HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"), HFInferenceProvider.AUTOMATIC => TB("Automatic: the fastest provider"),
HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"), HFInferenceProvider.CHEAPEST => TB("Automatic: the cheapest provider"),
@@ -156,3 +134,25 @@ public static class HFInferenceProviderExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
/// <param name="model">The model as it is configured.</param>
extension(Model model)
{
/// <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>
/// <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] };
}
}
}
@@ -30,6 +30,9 @@ public static class LLMProvidersExtensions
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LLMProvidersExtensions).Namespace, nameof(LLMProvidersExtensions)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(LLMProvidersExtensions).Namespace, nameof(LLMProvidersExtensions));
/// <param name="provider">The provider.</param>
extension(LLMProviders provider)
{
/// <summary> /// <summary>
/// Returns the human-readable name of the provider. /// Returns the human-readable name of the provider.
/// </summary> /// </summary>
@@ -37,17 +40,15 @@ public static class LLMProvidersExtensions
/// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces, /// This value is UI text and may be localized. Do not use it for persisted IDs, secret namespaces,
/// or other stable identifiers. /// or other stable identifiers.
/// </remarks> /// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The human-readable name of the provider.</returns> /// <returns>The human-readable name of the provider.</returns>
public static string ToName(this LLMProviders llmProvider) => llmProvider.ToName(translate: true); public string ToName() => provider.ToName(translate: true);
/// <summary> /// <summary>
/// Returns the human-readable name of the provider. /// Returns the human-readable name of the provider.
/// </summary> /// </summary>
/// <param name="llmProvider">The provider.</param>
/// <param name="translate">Whether generic provider names should be translated.</param> /// <param name="translate">Whether generic provider names should be translated.</param>
/// <returns>The human-readable name of the provider.</returns> /// <returns>The human-readable name of the provider.</returns>
public static string ToName(this LLMProviders llmProvider, bool translate) => llmProvider switch public string ToName(bool translate) => provider switch
{ {
LLMProviders.NONE => translate ? TB("No provider selected") : "No provider selected", LLMProviders.NONE => translate ? TB("No provider selected") : "No provider selected",
@@ -84,9 +85,8 @@ public static class LLMProvidersExtensions
/// These values are used for OS keyring namespaces. They must never be localized or changed without /// These values are used for OS keyring namespaces. They must never be localized or changed without
/// an explicit migration for existing API keys. /// an explicit migration for existing API keys.
/// </remarks> /// </remarks>
/// <param name="llmProvider">The provider.</param>
/// <returns>The stable secret namespace for the provider.</returns> /// <returns>The stable secret namespace for the provider.</returns>
public static string ToSecretId(this LLMProviders llmProvider) => llmProvider switch public string ToSecretId() => provider switch
{ {
LLMProviders.NONE => "No provider selected", LLMProviders.NONE => "No provider selected",
@@ -119,59 +119,58 @@ public static class LLMProvidersExtensions
/// <summary> /// <summary>
/// Get a provider's confidence. /// Get a provider's confidence.
/// </summary> /// </summary>
/// <param name="llmProvider">The provider.</param>
/// <param name="settingsManager">The settings manager.</param> /// <param name="settingsManager">The settings manager.</param>
/// <returns>The confidence of the provider.</returns> /// <returns>The confidence of the provider.</returns>
public static Confidence GetConfidence(this LLMProviders llmProvider, SettingsManager settingsManager) => llmProvider switch public Confidence GetConfidence(SettingsManager settingsManager) => provider switch
{ {
LLMProviders.NONE => Confidence.NONE, LLMProviders.NONE => Confidence.NONE,
LLMProviders.FIREWORKS => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.FIREWORKS => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://fireworks.ai/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
// Not trusted, because huggingface only routes you to a third-party-provider and we can't make sure they do not use your data // Not trusted, because huggingface only routes you to a third-party-provider and we can't make sure they do not use your data
LLMProviders.HUGGINGFACE => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://huggingface.co/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.HUGGINGFACE => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://huggingface.co/terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources( LLMProviders.OPEN_AI => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources(
"https://platform.openai.com/docs/models/default-usage-policies-by-endpoint", "https://platform.openai.com/docs/models/default-usage-policies-by-endpoint",
"https://openai.com/policies/terms-of-use/", "https://openai.com/policies/terms-of-use/",
"https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance", "https://help.openai.com/en/articles/5722486-how-your-data-is-used-to-improve-model-performance",
"https://openai.com/enterprise-privacy/" "https://openai.com/enterprise-privacy/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.GOOGLE => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://ai.google.dev/gemini-api/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.GOOGLE => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://ai.google.dev/gemini-api/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.GROQ => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://wow.groq.com/terms-of-use/").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.GROQ => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://wow.groq.com/terms-of-use/").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.ANTHROPIC => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.anthropic.com/legal/commercial-terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.MISTRAL => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, France").WithSources("https://mistral.ai/terms/#terms-of-service-la-plateforme").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.X => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://x.ai/legal/terms-of-service-enterprise").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.DEEP_SEEK => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://cdn.deepseek.com/policies/en-US/deepseek-open-platform-terms-of-service.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.ALIBABA_CLOUD => Confidence.CHINA_NO_TRAINING.WithRegion("Asia").WithSources("https://www.alibabacloud.com/help/en/model-studio/support/faq-about-alibaba-cloud-model-studio").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.PERPLEXITY => Confidence.USA_NO_TRAINING.WithRegion("America, U.S.").WithSources("https://www.perplexity.ai/hub/legal/perplexity-api-terms-of-service").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.OPEN_ROUTER => Confidence.USA_HUB.WithRegion("America, U.S.").WithSources("https://openrouter.ai/privacy", "https://openrouter.ai/terms").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources( LLMProviders.HETZNER => Confidence.GDPR_EXPERIMENTAL_OPEN_SOURCE.WithRegion("Europe, Germany").WithSources(
"https://experiments.hetzner.com/docs/inference", "https://experiments.hetzner.com/docs/inference",
"https://www.hetzner.com/legal/privacy-policy/", "https://www.hetzner.com/legal/privacy-policy/",
"https://www.hetzner.com/legal/terms-and-conditions/" "https://www.hetzner.com/legal/terms-and-conditions/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.IONOS => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources( LLMProviders.IONOS => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources(
"https://docs.ionos.com/cloud/ai/ai-model-hub/governance-and-compliance/data-handling", "https://docs.ionos.com/cloud/ai/ai-model-hub/governance-and-compliance/data-handling",
"https://www.ionos.com/terms-gtc/privacy-policy/" "https://www.ionos.com/terms-gtc/privacy-policy/"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
// LiteLLM is a gateway the user runs, but it is not a self-hosted LLM: the proxy owner decides // LiteLLM is a gateway the user runs, but it is not a self-hosted LLM: the proxy owner decides
// which downstream providers it routes to, and those are usually cloud services. Self-hosting // which downstream providers it routes to, and those are usually cloud services. Self-hosting
// the proxy therefore says nothing about where the data ends up, so we do not claim the trust // the proxy therefore says nothing about where the data ends up, so we do not claim the trust
// of a self-hosted model here and let the user assign the level themselves. // of a self-hosted model here and let the user assign the level themselves.
LLMProviders.LITE_LLM => Confidence.USER_OPERATED_GATEWAY.WithSources("https://docs.litellm.ai/docs/data_security").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.LITE_LLM => Confidence.USER_OPERATED_GATEWAY.WithSources("https://docs.litellm.ai/docs/data_security").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
// Requesty Ltd is based in London and works under the UK GDPR and the EU GDPR. Unless logging // Requesty Ltd is based in London and works under the UK GDPR and the EU GDPR. Unless logging
// is switched off, it keeps requests for up to 30 days, and it may use the content of free // is switched off, it keeps requests for up to 30 days, and it may use the content of free
@@ -183,22 +182,21 @@ public static class LLMProvidersExtensions
"https://www.requesty.ai/privacy/subprocessors", "https://www.requesty.ai/privacy/subprocessors",
"https://www.requesty.ai/dpa", "https://www.requesty.ai/dpa",
"https://www.requesty.ai/terms" "https://www.requesty.ai/terms"
).WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), ).WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.SELF_HOSTED => Confidence.SELF_HOSTED.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.HELMHOLTZ => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://helmholtz.cloud/services/?serviceID=d7d5c597-a2f6-4bd1-b71e-4d6499d98570").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/ai-services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), LLMProviders.GWDG => Confidence.GDPR_NO_TRAINING.WithRegion("Europe, Germany").WithSources("https://docs.hpc.gwdg.de/services/ai-services/chat-ai/data-privacy/index.html").WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
_ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(llmProvider)), _ => Confidence.UNKNOWN.WithLevel(settingsManager.GetConfiguredConfidenceLevel(provider)),
}; };
/// <summary> /// <summary>
/// Determines if the specified provider supports embeddings. /// Determines if the specified provider supports embeddings.
/// </summary> /// </summary>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>True if the provider supports embeddings; otherwise, false.</returns> /// <returns>True if the provider supports embeddings; otherwise, false.</returns>
public static bool ProvideEmbeddingAPI(this LLMProviders llmProvider) => llmProvider switch public bool ProvideEmbeddingAPI() => provider switch
{ {
// //
// Providers that support embeddings: // Providers that support embeddings:
@@ -241,7 +239,7 @@ public static class LLMProvidersExtensions
_ => false, _ => false,
}; };
public static bool ProvideTranscriptionAPI(this LLMProviders llmProvider) => llmProvider switch public bool ProvideTranscriptionAPI() => provider switch
{ {
// //
// Providers that support transcription: // Providers that support transcription:
@@ -296,9 +294,8 @@ public static class LLMProvidersExtensions
/// larger images through another provider. What we actually send is asked through /// larger images through another provider. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit. /// IImageSourceExtensions.MaxImageSizeBytesFor, which also applies our own limit.
/// </remarks> /// </remarks>
/// <param name="llmProvider">The provider to check.</param>
/// <returns>The size limit of one image file, or null when the provider states none we know of.</returns> /// <returns>The size limit of one image file, or null when the provider states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this LLMProviders llmProvider) => llmProvider switch public long? GetMaxImageSizeBytes() => provider switch
{ {
// //
// Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four // Anthropic accepts at most 5 MiB (5,242,880 bytes) of base64 per image. Base64 needs four
@@ -309,37 +306,7 @@ public static class LLMProvidersExtensions
_ => null, _ => null,
}; };
/// <summary> private IProvider CreateProvider(string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT, ProviderCapabilityOverrides? capabilityOverrides = null, string tokenizerPath = "")
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <param name="providerSettings">The provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this AIStudio.Settings.Provider providerSettings)
{
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration, capabilityOverrides: providerSettings.CapabilityOverrides, tokenizerPath: providerSettings.TokenizerPath);
}
/// <summary>
/// Creates a new provider instance based on the embedding provider value.
/// </summary>
/// <param name="embeddingProviderSettings">The embedding provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this EmbeddingProvider embeddingProviderSettings)
{
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING, tokenizerPath: embeddingProviderSettings.TokenizerPath);
}
/// <summary>
/// Creates a new provider instance based on the speech provider value.
/// </summary>
/// <param name="transcriptionProviderSettings">The speech provider settings.</param>
/// <returns>The provider instance.</returns>
public static IProvider CreateProvider(this TranscriptionProvider transcriptionProviderSettings)
{
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION);
}
private static IProvider CreateProvider(this LLMProviders provider, string instanceName, Host host, string hostname, HFInferenceProvider inferenceProvider, string configuredProviderId = "", string expertProviderApiParameter = "", bool isEnterpriseConfiguration = false, HFEndpointKind hfEndpointKind = HFEndpointKind.CHAT, ProviderCapabilityOverrides? capabilityOverrides = null, string tokenizerPath = "")
{ {
// //
// Every provider instance comes from here, for LLMs, embeddings, and transcriptions alike. // Every provider instance comes from here, for LLMs, embeddings, and transcriptions alike.
@@ -393,22 +360,7 @@ public static class LLMProvidersExtensions
} }
} }
/// <summary> public string GetCreationURL() => provider switch
/// Checks whether the organization prohibited the given provider.
/// </summary>
/// <remarks>
/// The settings come from Program.SERVICE_PROVIDER, because none of the callers of CreateProvider
/// hands them in. Tests which create providers without the services of the app have no settings
/// there, and nothing is prohibited for them.
/// </remarks>
private static bool IsProhibitedByOrganization(LLMProviders provider)
{
// Declared as never null, but it is null until the app has started its services:
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
return Program.SERVICE_PROVIDER?.GetService<SettingsManager>() is { } settingsManager && settingsManager.IsProviderProhibited(provider);
}
public static string GetCreationURL(this LLMProviders provider) => provider switch
{ {
LLMProviders.OPEN_AI => "https://platform.openai.com/signup", LLMProviders.OPEN_AI => "https://platform.openai.com/signup",
LLMProviders.MISTRAL => "https://console.mistral.ai/", LLMProviders.MISTRAL => "https://console.mistral.ai/",
@@ -433,7 +385,7 @@ public static class LLMProvidersExtensions
_ => string.Empty, _ => string.Empty,
}; };
public static string GetDashboardURL(this LLMProviders provider) => provider switch public string GetDashboardURL() => provider switch
{ {
LLMProviders.OPEN_AI => "https://platform.openai.com/usage", LLMProviders.OPEN_AI => "https://platform.openai.com/usage",
LLMProviders.MISTRAL => "https://console.mistral.ai/usage/", LLMProviders.MISTRAL => "https://console.mistral.ai/usage/",
@@ -456,7 +408,7 @@ public static class LLMProvidersExtensions
_ => string.Empty, _ => string.Empty,
}; };
public static bool HasDashboard(this LLMProviders provider) => provider switch public bool HasDashboard() => provider switch
{ {
LLMProviders.OPEN_AI => true, LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true, LLMProviders.MISTRAL => true,
@@ -479,14 +431,14 @@ public static class LLMProvidersExtensions
_ => false, _ => false,
}; };
public static string GetModelsOverviewURL(this LLMProviders provider, HFInferenceProvider inferenceProvider) => provider switch public string GetModelsOverviewURL(HFInferenceProvider inferenceProvider) => provider switch
{ {
LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless", LLMProviders.FIREWORKS => "https://fireworks.ai/models?show=Serverless",
LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}", LLMProviders.HUGGINGFACE => $"https://huggingface.co/models?inference_provider={inferenceProvider.CatalogFilter()}",
_ => string.Empty, _ => string.Empty,
}; };
public static bool IsLLMModelProvidedManually(this LLMProviders provider) => provider switch public bool IsLLMModelProvidedManually() => provider switch
{ {
LLMProviders.FIREWORKS => true, LLMProviders.FIREWORKS => true,
_ => false, _ => false,
@@ -496,10 +448,9 @@ public static class LLMProvidersExtensions
/// Determines if the model selection should be completely hidden for LLM providers. /// Determines if the model selection should be completely hidden for LLM providers.
/// This is the case when the host does not support model selection. /// This is the case when the host does not support model selection.
/// </summary> /// </summary>
/// <param name="provider">The provider.</param>
/// <param name="host">The host for self-hosted providers.</param> /// <param name="host">The host for self-hosted providers.</param>
/// <returns>True if model selection should be hidden; otherwise, false.</returns> /// <returns>True if model selection should be hidden; otherwise, false.</returns>
public static bool IsLLMModelSelectionHidden(this LLMProviders provider, Host host) => provider switch public bool IsLLMModelSelectionHidden(Host host) => provider switch
{ {
_ => false, _ => false,
}; };
@@ -508,22 +459,21 @@ public static class LLMProvidersExtensions
/// Determines if the model selection should be completely hidden for transcription providers. /// Determines if the model selection should be completely hidden for transcription providers.
/// This is the case when the host does not support model selection (e.g., whisper.cpp). /// This is the case when the host does not support model selection (e.g., whisper.cpp).
/// </summary> /// </summary>
/// <param name="provider">The provider.</param>
/// <param name="host">The host for self-hosted providers.</param> /// <param name="host">The host for self-hosted providers.</param>
/// <returns>True if model selection should be hidden; otherwise, false.</returns> /// <returns>True if model selection should be hidden; otherwise, false.</returns>
public static bool IsTranscriptionModelSelectionHidden(this LLMProviders provider, Host host) => provider switch public bool IsTranscriptionModelSelectionHidden(Host host) => provider switch
{ {
LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP, LLMProviders.SELF_HOSTED => host is Host.WHISPER_CPP,
_ => false, _ => false,
}; };
public static bool IsHostNeeded(this LLMProviders provider) => provider switch public bool IsHostNeeded() => provider switch
{ {
LLMProviders.SELF_HOSTED => true, LLMProviders.SELF_HOSTED => true,
_ => false, _ => false,
}; };
public static bool IsHostnameNeeded(this LLMProviders provider) => provider switch public bool IsHostnameNeeded() => provider switch
{ {
LLMProviders.SELF_HOSTED => true, LLMProviders.SELF_HOSTED => true,
LLMProviders.LITE_LLM => true, LLMProviders.LITE_LLM => true,
@@ -540,16 +490,15 @@ public static class LLMProvidersExtensions
/// today: whether a dialog asks for a hostname is a question of the user interface, and it must /// today: whether a dialog asks for a hostname is a question of the user interface, and it must
/// never silently decide what a server learns about the user. /// never silently decide what a server learns about the user.
/// </remarks> /// </remarks>
/// <param name="provider">The provider.</param>
/// <returns>True when the provider runs on a self-hosted server; otherwise, false.</returns> /// <returns>True when the provider runs on a self-hosted server; otherwise, false.</returns>
public static bool RunsOnSelfHostedServer(this LLMProviders provider) => provider switch public bool RunsOnSelfHostedServer() => provider switch
{ {
LLMProviders.SELF_HOSTED => true, LLMProviders.SELF_HOSTED => true,
LLMProviders.LITE_LLM => true, LLMProviders.LITE_LLM => true,
_ => false, _ => false,
}; };
public static bool IsAPIKeyNeeded(this LLMProviders provider, Host host) => provider switch public bool IsAPIKeyNeeded(Host host) => provider switch
{ {
LLMProviders.OPEN_AI => true, LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true, LLMProviders.MISTRAL => true,
@@ -580,7 +529,7 @@ public static class LLMProvidersExtensions
_ => false, _ => false,
}; };
public static bool ShowRegisterButton(this LLMProviders provider) => provider switch public bool ShowRegisterButton() => provider switch
{ {
LLMProviders.OPEN_AI => true, LLMProviders.OPEN_AI => true,
LLMProviders.MISTRAL => true, LLMProviders.MISTRAL => true,
@@ -604,7 +553,7 @@ public static class LLMProvidersExtensions
_ => false, _ => false,
}; };
public static bool CanLoadModels(this LLMProviders provider, Host host, string? apiKey) public bool CanLoadModels(Host host, string? apiKey)
{ {
if (provider is LLMProviders.SELF_HOSTED) if (provider is LLMProviders.SELF_HOSTED)
return host.IsModelListSupported(); return host.IsModelListSupported();
@@ -618,9 +567,64 @@ public static class LLMProvidersExtensions
return true; return true;
} }
public static bool IsHFInstanceProviderNeeded(this LLMProviders provider) => provider switch public bool IsHFInstanceProviderNeeded() => provider switch
{ {
LLMProviders.HUGGINGFACE => true, LLMProviders.HUGGINGFACE => true,
_ => false, _ => false,
}; };
} }
/// <param name="providerSettings">The provider settings.</param>
extension(AIStudio.Settings.Provider providerSettings)
{
/// <summary>
/// Creates a new provider instance based on the provider value.
/// </summary>
/// <returns>The provider instance.</returns>
public IProvider CreateProvider()
{
return providerSettings.UsedLLMProvider.CreateProvider(providerSettings.InstanceName, providerSettings.Host, providerSettings.Hostname, providerSettings.HFInferenceProvider, providerSettings.Id, providerSettings.AdditionalJsonApiParameters, providerSettings.IsEnterpriseConfiguration, capabilityOverrides: providerSettings.CapabilityOverrides, tokenizerPath: providerSettings.TokenizerPath);
}
}
/// <param name="embeddingProviderSettings">The embedding provider settings.</param>
extension(EmbeddingProvider embeddingProviderSettings)
{
/// <summary>
/// Creates a new provider instance based on the embedding provider value.
/// </summary>
/// <returns>The provider instance.</returns>
public IProvider CreateProvider()
{
return embeddingProviderSettings.UsedLLMProvider.CreateProvider(embeddingProviderSettings.Name, embeddingProviderSettings.Host, embeddingProviderSettings.Hostname, embeddingProviderSettings.HFInferenceProvider, configuredProviderId: embeddingProviderSettings.Id, isEnterpriseConfiguration: embeddingProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.EMBEDDING, tokenizerPath: embeddingProviderSettings.TokenizerPath);
}
}
/// <param name="transcriptionProviderSettings">The speech provider settings.</param>
extension(TranscriptionProvider transcriptionProviderSettings)
{
/// <summary>
/// Creates a new provider instance based on the speech provider value.
/// </summary>
/// <returns>The provider instance.</returns>
public IProvider CreateProvider()
{
return transcriptionProviderSettings.UsedLLMProvider.CreateProvider(transcriptionProviderSettings.Name, transcriptionProviderSettings.Host, transcriptionProviderSettings.Hostname, transcriptionProviderSettings.HFInferenceProvider, configuredProviderId: transcriptionProviderSettings.Id, isEnterpriseConfiguration: transcriptionProviderSettings.IsEnterpriseConfiguration, hfEndpointKind: HFEndpointKind.TRANSCRIPTION);
}
}
/// <summary>
/// Checks whether the organization prohibited the given provider.
/// </summary>
/// <remarks>
/// The settings come from Program.SERVICE_PROVIDER, because none of the callers of CreateProvider
/// hands them in. Tests which create providers without the services of the app have no settings
/// there, and nothing is prohibited for them.
/// </remarks>
private static bool IsProhibitedByOrganization(LLMProviders provider)
{
// Declared as never null, but it is null until the app has started its services:
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
return Program.SERVICE_PROVIDER?.GetService<SettingsManager>() is { } settingsManager && settingsManager.IsProviderProhibited(provider);
}
}
@@ -5,10 +5,15 @@ 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)
{
public string GetIconUrl(bool isDarkMode)
=> provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl); => provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl);
}
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl) extension(LLMProviders provider)
{
public string GetIconUrl(bool isDarkMode, string? customIconDataUrl)
{ {
if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true) if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true)
return customIconDataUrl; return customIconDataUrl;
@@ -16,7 +21,7 @@ public static class LLMProvidersIconExtensions
return provider.GetIconUrl(isDarkMode); return provider.GetIconUrl(isDarkMode);
} }
public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch public string GetIconUrl(bool isDarkMode) => provider switch
{ {
LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg",
LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg", LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg",
@@ -40,6 +45,7 @@ public static class LLMProvidersIconExtensions
LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg", LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg",
_ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.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,7 +6,9 @@ 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)
{
public string ToUserMessage(string providerName) => failureReason switch
{ {
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.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.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.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),
@@ -19,3 +21,4 @@ public static class ModelLoadFailureReasonExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -3,6 +3,9 @@ using AIStudio.Models;
namespace AIStudio.Provider.OpenAI; namespace AIStudio.Provider.OpenAI;
public static class SystemPromptRoleExtensions public static class SystemPromptRoleExtensions
{
/// <param name="role">The role the model profile states.</param>
extension(SystemPromptRole role)
{ {
/// <summary> /// <summary>
/// Spells a system prompt role the way OpenAI's APIs expect it. /// Spells a system prompt role the way OpenAI's APIs expect it.
@@ -12,9 +15,8 @@ public static class SystemPromptRoleExtensions
/// model the rules do not know is almost always one released after they were written, and the /// 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. /// older generations which still want "system" say so in their families.
/// </remarks> /// </remarks>
/// <param name="role">The role the model profile states.</param>
/// <returns>The role, as OpenAI names it.</returns> /// <returns>The role, as OpenAI names it.</returns>
public static string ToOpenAIRole(this SystemPromptRole role) => role switch public string ToOpenAIRole() => role switch
{ {
SystemPromptRole.SYSTEM => "system", SystemPromptRole.SYSTEM => "system",
SystemPromptRole.DEVELOPER => "developer", SystemPromptRole.DEVELOPER => "developer",
@@ -23,3 +25,4 @@ public static class SystemPromptRoleExtensions
_ => "developer", _ => "developer",
}; };
} }
}
@@ -6,6 +6,8 @@ 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));
extension(ProviderRequestFailureReason failureReason)
{
/// <summary> /// <summary>
/// Names the kind of failure in a few words. /// Names the kind of failure in a few words.
/// </summary> /// </summary>
@@ -13,7 +15,7 @@ public static class ProviderRequestFailureReasonExtensions
/// Meant as a label beside the full message, so a list of failures can be scanned instead of /// 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. /// read: twenty entries which all say API key are one problem, not twenty.
/// </remarks> /// </remarks>
public static string GetName(this ProviderRequestFailureReason failureReason) => failureReason switch public string GetName() => failureReason switch
{ {
ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"), ProviderRequestFailureReason.INSUFFICIENT_QUOTA => TB("No credits left"),
ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"), ProviderRequestFailureReason.TOO_MANY_REQUESTS => TB("Too many requests"),
@@ -37,7 +39,7 @@ public static class ProviderRequestFailureReasonExtensions
/// 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
@@ -45,3 +47,4 @@ public static class ProviderRequestFailureReasonExtensions
ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or ProviderRequestFailureReason.EMBEDDINGS_NOT_SUPPORTED or
ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED; ProviderRequestFailureReason.CONTEXT_LENGTH_EXCEEDED;
} }
}
@@ -2,7 +2,10 @@ namespace AIStudio.Provider.SelfHosted;
public static class HostExtensions public static class HostExtensions
{ {
public static string Name(this Host host) => host switch /// <param name="host">The engine.</param>
extension(Host host)
{
public string Name() => host switch
{ {
Host.NONE => "None", Host.NONE => "None",
@@ -16,22 +19,22 @@ public static class HostExtensions
_ => "Unknown", _ => "Unknown",
}; };
public static string BaseURL(this Host host) => host switch public string BaseURL() => host switch
{ {
_ => "/v1/", _ => "/v1/",
}; };
public static string ChatURL(this Host host) => host switch public string ChatURL() => host switch
{ {
_ => "chat/completions", _ => "chat/completions",
}; };
public static string TranscriptionURL(this Host host) => host switch public string TranscriptionURL() => host switch
{ {
_ => "audio/transcriptions", _ => "audio/transcriptions",
}; };
public static string EmbeddingURL(this Host host) => host switch public string EmbeddingURL() => host switch
{ {
_ => "embeddings", _ => "embeddings",
}; };
@@ -44,9 +47,8 @@ public static class HostExtensions
/// belongs to the API of the engine, not to the model. What we actually send is asked through /// belongs to the API of the engine, not to the model. What we actually send is asked through
/// IImageSourceExtensions.MaxImageSizeBytesFor. /// IImageSourceExtensions.MaxImageSizeBytesFor.
/// </remarks> /// </remarks>
/// <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> /// <returns>The size limit of one image file, or null when the engine states none we know of.</returns>
public static long? GetMaxImageSizeBytes(this Host host) => host switch public long? GetMaxImageSizeBytes() => host switch
{ {
// //
// llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions // llmman answers a chat request larger than 2 MiB with 413: its route /v1/chat/completions
@@ -59,7 +61,7 @@ public static class HostExtensions
_ => null, _ => null,
}; };
public static bool IsChatSupported(this Host host) public bool IsChatSupported()
{ {
switch (host) switch (host)
{ {
@@ -76,7 +78,7 @@ public static class HostExtensions
} }
} }
public static bool IsEmbeddingSupported(this Host host) public bool IsEmbeddingSupported()
{ {
switch (host) switch (host)
{ {
@@ -92,7 +94,7 @@ public static class HostExtensions
} }
} }
public static bool IsTranscriptionSupported(this Host host) public bool IsTranscriptionSupported()
{ {
switch (host) switch (host)
{ {
@@ -120,9 +122,8 @@ public static class HostExtensions
/// 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 static bool IsModelListSupported(this Host host) public bool IsModelListSupported()
{ {
switch (host) switch (host)
{ {
@@ -140,3 +141,4 @@ public static class HostExtensions
} }
} }
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class BiasCategoryExtensions public static class BiasCategoryExtensions
{ {
public static string ToName(this BiasCategory biasCategory) => biasCategory switch extension(BiasCategory biasCategory)
{
public string ToName() => biasCategory switch
{ {
BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?", BiasCategory.WHAT_SHOULD_WE_REMEMBER => "What should we remember?",
BiasCategory.TOO_MUCH_INFORMATION => "Too much information", BiasCategory.TOO_MUCH_INFORMATION => "Too much information",
@@ -12,7 +14,7 @@ public static class BiasCategoryExtensions
_ => "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 =>
""" """
@@ -53,3 +55,4 @@ public static class BiasCategoryExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string ToSelectionText() => security switch
{ {
DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"), DataSourceSecurity.NOT_SPECIFIED => TB("Please select a security policy"),
@@ -16,7 +18,7 @@ public static class DataSourceSecurityExtensions
_ => 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."),
@@ -26,6 +28,18 @@ public static class DataSourceSecurityExtensions
_ => 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
};
}
/// <param name="held">The data security held so far.</param>
extension(DataSourceSecurity held)
{
/// <summary> /// <summary>
/// The data security which demands what the one held and the one arriving demand together. /// The data security which demands what the one held and the one arriving demand together.
/// </summary> /// </summary>
@@ -35,21 +49,13 @@ public static class DataSourceSecurityExtensions
/// as one which holds data of a data source, while a restriction set earlier stays. NOT_SPECIFIED /// 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. /// demands nothing and changes nothing. The order does not matter.
/// </remarks> /// </remarks>
/// <param name="held">The data security held so far.</param>
/// <param name="arriving">What the data arriving demands.</param> /// <param name="arriving">What the data arriving demands.</param>
/// <returns>The tightened data security; never looser than either of the two.</returns> /// <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 public DataSourceSecurity TightenedBy(DataSourceSecurity arriving) => (held, arriving) switch
{ {
(DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED, (DataSourceSecurity.SELF_HOSTED, _) or (_, DataSourceSecurity.SELF_HOSTED) => DataSourceSecurity.SELF_HOSTED,
(_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY, (_, DataSourceSecurity.ALLOW_ANY) => DataSourceSecurity.ALLOW_ANY,
_ => held, _ => held,
}; };
}
public static TextColor GetColor(this DataSourceSecurity security) => security switch
{
DataSourceSecurity.ALLOW_ANY => TextColor.WARN,
DataSourceSecurity.SELF_HOSTED => TextColor.SUCCESS,
_ => TextColor.ERROR
};
} }
@@ -9,12 +9,14 @@ 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));
/// <param name="type">The data source type.</param>
extension(DataSourceType type)
{
/// <summary> /// <summary>
/// Get the display name of the data source type. /// Get the display name of the data source type.
/// </summary> /// </summary>
/// <param name="type">The data source type.</param>
/// <returns>The display name of the data source type.</returns> /// <returns>The display name of the data source type.</returns>
public static string GetDisplayName(this DataSourceType type) => type switch public string GetDisplayName() => type switch
{ {
DataSourceType.LOCAL_FILE => TB("Local File"), DataSourceType.LOCAL_FILE => TB("Local File"),
DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"), DataSourceType.LOCAL_DIRECTORY => TB("Local Directory"),
@@ -24,3 +26,4 @@ public static class DataSourceTypeExtension
_ => TB("None"), _ => TB("None"),
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => langBehavior switch
{ {
LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."), LangBehavior.AUTO => TB("Choose the language automatically, based on your system language."),
LangBehavior.MANUAL => TB("Choose the language manually."), LangBehavior.MANUAL => TB("Choose the language manually."),
@@ -14,3 +16,4 @@ public static class LangBehaviorExtensions
_ => TB("Unknown option") _ => TB("Unknown option")
}; };
} }
}
@@ -6,7 +6,10 @@ 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 /// <param name="maxAge">The period.</param>
extension(MailboxMaxAge maxAge)
{
public string GetName() => maxAge switch
{ {
MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"), MailboxMaxAge.LAST_3_MONTHS => TB("The last 3 months"),
MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"), MailboxMaxAge.LAST_6_MONTHS => TB("The last 6 months"),
@@ -20,10 +23,9 @@ public static class MailboxMaxAgeExtensions
/// <summary> /// <summary>
/// The first day of the period, as the sync asks the server for it. /// The first day of the period, as the sync asks the server for it.
/// </summary> /// </summary>
/// <param name="maxAge">The period.</param>
/// <param name="now">The current time.</param> /// <param name="now">The current time.</param>
/// <returns>The first day, or null when the period has no end.</returns> /// <returns>The first day, or null when the period has no end.</returns>
public static DateTimeOffset? GetReceivedSince(this MailboxMaxAge maxAge, DateTimeOffset now) => maxAge switch public DateTimeOffset? GetReceivedSince(DateTimeOffset now) => maxAge switch
{ {
MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6), MailboxMaxAge.LAST_6_MONTHS => now.AddMonths(-6),
MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12), MailboxMaxAge.LAST_12_MONTHS => now.AddMonths(-12),
@@ -34,3 +36,4 @@ public static class MailboxMaxAgeExtensions
_ => now.AddMonths(-3), _ => now.AddMonths(-3),
}; };
} }
}
@@ -16,7 +16,9 @@ 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)
{
public string GetName() => transportSecurity switch
{ {
MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"), MailboxTransportSecurity.SSL_ON_CONNECT => TB("TLS from the start (usually port 993)"),
MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"), MailboxTransportSecurity.STARTTLS => TB("STARTTLS (usually port 143)"),
@@ -28,7 +30,7 @@ public static class MailboxTransportSecurityExtensions
/// 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,
@@ -36,3 +38,4 @@ public static class MailboxTransportSecurityExtensions
_ => null, _ => null,
}; };
} }
}
@@ -6,6 +6,9 @@ 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));
/// <param name="requirement">What the chat demands.</param>
extension(OutboundDataRequirement requirement)
{
/// <summary> /// <summary>
/// Why a tool cannot run in a chat with this requirement, and how to get it back. /// Why a tool cannot run in a chat with this requirement, and how to get it back.
/// </summary> /// </summary>
@@ -17,10 +20,9 @@ public static class OutboundDataRequirementExtensions
/// The text speaks of content the chat holds, not of content it read: the content may come from /// 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. /// another chat or an assistant, which sent it here along with its restrictions.
/// </remarks> /// </remarks>
/// <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> /// <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> /// <returns>The text for the user.</returns>
public static string GetToolBlockedMessage(this OutboundDataRequirement requirement, Data settings) => requirement.Origin switch public string GetToolBlockedMessage(Data settings) => requirement.Origin switch
{ {
OutboundDataOrigin.MAILBOX => GetMailboxBlockedMessage(requirement, settings.Mailboxes), 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.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()),
@@ -31,6 +33,7 @@ public static class OutboundDataRequirementExtensions
// 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,16 +6,18 @@ 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));
/// <param name="restriction">The restriction, as it was read.</param>
extension(OutboundDataRestriction restriction)
{
/// <summary> /// <summary>
/// The stricter of two restrictions. /// The stricter of two restrictions.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown. /// A restriction this version does not know counts as the strictest one, see OrStrictestWhenUnknown.
/// </remarks> /// </remarks>
/// <param name="restriction">The one restriction.</param>
/// <param name="other">The other restriction.</param> /// <param name="other">The other restriction.</param>
/// <returns>The restriction which allows less.</returns> /// <returns>The restriction which allows less.</returns>
public static OutboundDataRestriction StricterOf(this OutboundDataRestriction restriction, OutboundDataRestriction other) public OutboundDataRestriction StricterOf(OutboundDataRestriction other)
{ {
var knownRestriction = restriction.OrStrictestWhenUnknown(); var knownRestriction = restriction.OrStrictestWhenUnknown();
var knownOther = other.OrStrictestWhenUnknown(); var knownOther = other.OrStrictestWhenUnknown();
@@ -30,11 +32,10 @@ public static class OutboundDataRestrictionExtensions
/// by hand, and the settings read it all the same. Compared as it is, such a number would rank /// 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. /// above UNRESTRICTED and lift every restriction it meets.
/// </remarks> /// </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> /// <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 OutboundDataRestriction OrStrictestWhenUnknown() => Enum.IsDefined(restriction) ? restriction : OutboundDataRestriction.ONLY_CONFIGURED_SERVICES;
public static string GetName(this OutboundDataRestriction restriction) => restriction switch public string GetName() => restriction switch
{ {
OutboundDataRestriction.ONLY_CONFIGURED_SERVICES => TB("Only services configured in AI Studio"), 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.ONLY_LINKS_FROM_CHAT => TB("Configured services and addresses from the chat"),
@@ -46,7 +47,7 @@ public static class OutboundDataRestrictionExtensions
/// <summary> /// <summary>
/// What a chat may still do on this level, once internal content entered it. /// What a chat may still do on this level, once internal content entered it.
/// </summary> /// </summary>
public static string GetDescription(this OutboundDataRestriction restriction) => restriction switch 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_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.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."),
@@ -55,3 +56,4 @@ public static class OutboundDataRestrictionExtensions
_ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."), _ => TB("This version of AI Studio does not know this restriction, so it applies the strictest one."),
}; };
} }
}
@@ -6,7 +6,10 @@ 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 /// <param name="feature">The preview feature to check.</param>
extension(PreviewFeatures feature)
{
public string GetPreviewDescription() => feature switch
{ {
PreviewFeatures.PRE_WRITER_MODE_2024 => TB("Writer Mode: Experiments about how to write long texts using AI"), 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_RAG_2024 => TB("RAG: Preview of our RAG implementation where you can refer your files or integrate enterprise data within your company"),
@@ -29,9 +32,8 @@ public static class PreviewFeaturesExtensions
/// This metadata is necessary because we cannot remove the enum values from the list. /// 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. /// Otherwise, we could not deserialize old settings files that may contain these values.
/// </remarks> /// </remarks>
/// <param name="feature">The preview feature to check.</param>
/// <returns>True when the preview feature is released, false otherwise.</returns> /// <returns>True when the preview feature is released, false otherwise.</returns>
public static bool IsReleased(this PreviewFeatures feature) => feature switch public bool IsReleased() => feature switch
{ {
PreviewFeatures.PRE_READ_PDF_2025 => true, PreviewFeatures.PRE_READ_PDF_2025 => true,
PreviewFeatures.PRE_PLUGINS_2025 => true, PreviewFeatures.PRE_PLUGINS_2025 => true,
@@ -41,7 +43,7 @@ public static class PreviewFeaturesExtensions
_ => false _ => false
}; };
public static bool IsEnabled(this PreviewFeatures feature, SettingsManager settingsManager) public bool IsEnabled(SettingsManager settingsManager)
{ {
if(feature.IsReleased()) if(feature.IsReleased())
return true; return true;
@@ -49,3 +51,4 @@ public static class PreviewFeaturesExtensions
return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature); return settingsManager.ConfigurationData.App.EnabledPreviewFeatures.Contains(feature);
} }
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class PreviewVisibilityExtensions public static class PreviewVisibilityExtensions
{ {
public static IList<PreviewFeatures> GetPreviewFeatures(this PreviewVisibility visibility) extension(PreviewVisibility visibility)
{
public IList<PreviewFeatures> GetPreviewFeatures()
{ {
var features = new List<PreviewFeatures>(); var features = new List<PreviewFeatures>();
if (visibility >= PreviewVisibility.RELEASE_CANDIDATE) if (visibility >= PreviewVisibility.RELEASE_CANDIDATE)
@@ -34,7 +36,7 @@ public static class PreviewVisibilityExtensions
return features; return features;
} }
public static HashSet<PreviewFeatures> FilterPreviewFeatures(this PreviewVisibility visibility, HashSet<PreviewFeatures> enabledFeatures) public HashSet<PreviewFeatures> FilterPreviewFeatures(HashSet<PreviewFeatures> enabledFeatures)
{ {
var filteredFeatures = new HashSet<PreviewFeatures>(); var filteredFeatures = new HashSet<PreviewFeatures>();
var previewFeatures = visibility.GetPreviewFeatures(); var previewFeatures = visibility.GetPreviewFeatures();
@@ -47,3 +49,4 @@ public static class PreviewVisibilityExtensions
return filteredFeatures; return filteredFeatures;
} }
} }
}
@@ -2,12 +2,17 @@ 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)
{
public List<AIStudio.Settings.Provider> MigrateFromV4ToV5()
{ {
return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList(); return providers.Select(provider => provider.MigrateFromV4ToV5()).ToList();
} }
}
public static AIStudio.Settings.Provider MigrateFromV4ToV5(this Provider provider) => new() extension(Provider provider)
{
public AIStudio.Settings.Provider MigrateFromV4ToV5() => new()
{ {
Num = provider.Num, Num = provider.Num,
Id = provider.Id, Id = provider.Id,
@@ -19,3 +24,4 @@ public static class ProviderV4Extensions
Host = provider.Host, Host = provider.Host,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Description() => behavior switch
{ {
SendToChatDataSourceBehavior.NO_DATA_SOURCES => TB("Use no data sources, 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"), SendToChatDataSourceBehavior.APPLY_STANDARD_CHAT_DATA_SOURCE_OPTIONS => TB("Apply standard chat data source options, when sending an assistant result to a chat"),
@@ -14,3 +16,4 @@ public static class SendToChatDataSourceBehaviorExtensions
_ => TB("Unknown behavior"), _ => TB("Unknown behavior"),
}; };
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Settings.DataModel;
public static class StartPageExtensions public static class StartPageExtensions
{ {
public static string ToRoute(this StartPage startPage) => startPage switch extension(StartPage startPage)
{
public string ToRoute() => startPage switch
{ {
StartPage.HOME => string.Empty, StartPage.HOME => string.Empty,
StartPage.CHAT => Routes.CHAT, StartPage.CHAT => Routes.CHAT,
@@ -15,3 +17,4 @@ public static class StartPageExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string GetName() => theme switch
{ {
Themes.SYSTEM => TB("Synchronized with the operating system settings"), Themes.SYSTEM => TB("Synchronized with the operating system settings"),
Themes.LIGHT => TB("Always use light theme"), Themes.LIGHT => TB("Always use light theme"),
@@ -15,3 +17,4 @@ public static class ThemesExtensions
_ => TB("Unknown setting"), _ => TB("Unknown setting"),
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string GetName() => bitrate switch
{ {
TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"), TranscriptionOpusBitrate.KBPS_32 => TB("32 kbps (smallest upload, lowest accuracy)"),
TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"), TranscriptionOpusBitrate.KBPS_64 => TB("64 kbps"),
@@ -15,7 +17,7 @@ public static class TranscriptionOpusBitrateExtensions
_ => TB("Unknown"), _ => 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,
@@ -27,3 +29,4 @@ public static class TranscriptionOpusBitrateExtensions
_ => 128_000, _ => 128_000,
}; };
} }
}
@@ -1,6 +1,9 @@
namespace AIStudio.Settings; namespace AIStudio.Settings;
public static class DataSourceOrderExtensions public static class DataSourceOrderExtensions
{
/// <param name="dataSources">The data sources to sort.</param>
extension(IEnumerable<IDataSource> dataSources)
{ {
/// <summary> /// <summary>
/// Sorts data sources in a fixed order: by their number, then by their ID. /// Sorts data sources in a fixed order: by their number, then by their ID.
@@ -12,10 +15,10 @@ public static class DataSourceOrderExtensions
/// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of /// see IToolImplementation.ResolveFunctionAsync. DataSourceRequirements names the first of
/// several equally strict data sources this way, so that both ways of searching name the same. /// several equally strict data sources this way, so that both ways of searching name the same.
/// </remarks> /// </remarks>
/// <param name="dataSources">The data sources to sort.</param>
/// <returns>The data sources in their fixed order.</returns> /// <returns>The data sources in their fixed order.</returns>
public static IReadOnlyList<IDataSource> InFixedOrder(this IEnumerable<IDataSource> dataSources) => dataSources public IReadOnlyList<IDataSource> InFixedOrder() => dataSources
.OrderBy(dataSource => dataSource.Num) .OrderBy(dataSource => dataSource.Num)
.ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal) .ThenBy(dataSource => dataSource.Id, StringComparer.Ordinal)
.ToList(); .ToList();
} }
}
@@ -30,13 +30,15 @@ public static class DataSourceOutboundDataExtensions
? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES) ? organizationMinimum.StricterOf(OutboundDataRestriction.ONLY_CONFIGURED_SERVICES)
: organizationMinimum; : organizationMinimum;
/// <param name="dataSource">The data source.</param>
extension(IDataSource dataSource)
{
/// <summary> /// <summary>
/// The least strict level this data source may have. /// The least strict level this data source may have.
/// </summary> /// </summary>
/// <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 least strict level allowed.</returns> /// <returns>The least strict level allowed.</returns>
public static OutboundDataRestriction GetMinimumOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => public OutboundDataRestriction GetMinimumOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum); GetMinimumOutboundDataRestriction(dataSource is IExternalDataSource externalDataSource ? externalDataSource.SecurityPolicy : DataSourceSecurity.NOT_SPECIFIED, organizationMinimum);
/// <summary> /// <summary>
@@ -47,9 +49,9 @@ public static class DataSourceOutboundDataExtensions
/// 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 static OutboundDataRestriction GetEffectiveOutboundDataRestriction(this IDataSource dataSource, OutboundDataRestriction organizationMinimum) => public OutboundDataRestriction GetEffectiveOutboundDataRestriction(OutboundDataRestriction organizationMinimum) =>
dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum)); dataSource.OutboundDataRestriction.StricterOf(dataSource.GetMinimumOutboundDataRestriction(organizationMinimum));
} }
}
@@ -5,7 +5,9 @@ namespace AIStudio.Settings;
public static class DataSourceSecurityTrustExtensions public static class DataSourceSecurityTrustExtensions
{ {
public static bool IsTrustedForDataSourceSecurityChecks(this Provider provider, SettingsManager settingsManager) extension(Provider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{ {
if (provider == Provider.NONE) if (provider == Provider.NONE)
return false; return false;
@@ -13,7 +15,29 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
} }
public static bool IsTrustedForDataSourceSecurityChecks(this EmbeddingProvider provider, SettingsManager settingsManager) 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);
}
extension(EmbeddingProvider provider)
{
public bool IsTrustedForDataSourceSecurityChecks(SettingsManager settingsManager)
{ {
if (provider == EmbeddingProvider.NONE) if (provider == EmbeddingProvider.NONE)
return false; return false;
@@ -21,7 +45,20 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
} }
public static bool IsTrustedForDataSourceSecurityChecks(this TranscriptionProvider provider, SettingsManager 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) if (provider == TranscriptionProvider.NONE)
return false; return false;
@@ -29,7 +66,20 @@ public static class DataSourceSecurityTrustExtensions
return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager); return provider.IsSelfHosted || provider.IsTrustedByConfiguration(settingsManager);
} }
public static bool IsTrustedForDataSourceSecurityChecks(this IProvider provider, SettingsManager 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) if (provider is NoProvider)
return false; return false;
@@ -37,31 +87,7 @@ public static class DataSourceSecurityTrustExtensions
return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager); return provider.Provider is LLMProviders.SELF_HOSTED || IsTrustedProviderId(provider.ConfiguredProviderId, settingsManager);
} }
public static ConfidenceLevel GetConfidenceLevel(this Provider provider, SettingsManager settingsManager) public ConfidenceLevel GetConfidenceLevel(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) if (provider is NoProvider)
return ConfidenceLevel.NONE; return ConfidenceLevel.NONE;
@@ -69,32 +95,29 @@ public static class DataSourceSecurityTrustExtensions
return provider.Provider.GetConfidence(settingsManager).Level; return provider.Provider.GetConfidence(settingsManager).Level;
} }
public static bool AllowsDataSourceAccess(this Provider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) public bool AllowsDataSourceAccess(SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel)
{ {
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager) return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel); && provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
} }
public static bool AllowsDataSourceAccess(this IProvider provider, SettingsManager settingsManager, DataSourceSecurity dataSourceSecurity, ConfidenceLevel requiredConfidenceLevel) public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager)
{ => provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity);
return provider.AllowsDataSourceSecurity(dataSourceSecurity, settingsManager)
&& provider.GetConfidenceLevel(settingsManager).AllowsDataSourceConfidenceLevel(requiredConfidenceLevel);
} }
public static bool AllowsDataSourceSecurity(this Provider provider, DataSourceSecurity dataSourceSecurity, SettingsManager settingsManager) extension(bool usingTrustedProvider)
=> provider.IsTrustedForDataSourceSecurityChecks(settingsManager).AllowsDataSourceSecurity(dataSourceSecurity); {
public bool AllowsDataSourceSecurity(DataSourceSecurity dataSourceSecurity) => dataSourceSecurity switch
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.ALLOW_ANY => true,
DataSourceSecurity.SELF_HOSTED => usingTrustedProvider, DataSourceSecurity.SELF_HOSTED => usingTrustedProvider,
_ => false, _ => false,
}; };
}
public static bool AllowsDataSourceConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel requiredConfidenceLevel) extension(ConfidenceLevel providerConfidenceLevel)
{
public bool AllowsDataSourceConfidenceLevel(ConfidenceLevel requiredConfidenceLevel)
{ {
if (requiredConfidenceLevel is ConfidenceLevel.NONE) if (requiredConfidenceLevel is ConfidenceLevel.NONE)
return true; return true;
@@ -102,16 +125,6 @@ public static class DataSourceSecurityTrustExtensions
return providerConfidenceLevel >= requiredConfidenceLevel; 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> /// <summary>
/// Whether a provider of this confidence level may see the content of a mailbox. /// Whether a provider of this confidence level may see the content of a mailbox.
/// </summary> /// </summary>
@@ -121,15 +134,31 @@ public static class DataSourceSecurityTrustExtensions
/// which sees mail content has to pass it: the chat provider, the embedding provider, and the /// which sees mail content has to pass it: the chat provider, the embedding provider, and the
/// provider of every agent reading mails. /// provider of every agent reading mails.
/// </remarks> /// </remarks>
public static bool AllowsMailboxConfidenceLevel(this ConfidenceLevel providerConfidenceLevel, ConfidenceLevel mailboxConfidenceLevel) public bool AllowsMailboxConfidenceLevel(ConfidenceLevel mailboxConfidenceLevel)
{ {
if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence()) if (!mailboxConfidenceLevel.IsAllowedMailboxConfidence())
return false; return false;
return providerConfidenceLevel >= mailboxConfidenceLevel; return providerConfidenceLevel >= mailboxConfidenceLevel;
} }
}
public static ConfidenceLevel GetRequiredConfidenceLevel(this IEnumerable<IDataSource> dataSources) 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; var requiredConfidenceLevel = ConfidenceLevel.NONE;
foreach (var dataSource in dataSources.OfType<IInternalDataSource>()) foreach (var dataSource in dataSources.OfType<IInternalDataSource>())
@@ -139,7 +168,7 @@ public static class DataSourceSecurityTrustExtensions
return requiredConfidenceLevel; return requiredConfidenceLevel;
} }
public static DataSourceSecurity GetRequiredSecurityPolicy(this IEnumerable<IDataSource> dataSources) public DataSourceSecurity GetRequiredSecurityPolicy()
{ {
var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY; var requiredSecurityPolicy = DataSourceSecurity.ALLOW_ANY;
foreach (var dataSource in dataSources.OfType<IExternalDataSource>()) foreach (var dataSource in dataSources.OfType<IExternalDataSource>())
@@ -153,12 +182,7 @@ public static class DataSourceSecurityTrustExtensions
return requiredSecurityPolicy; return requiredSecurityPolicy;
} }
}
public static bool IsTrustedByConfiguration(this Provider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this EmbeddingProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
public static bool IsTrustedByConfiguration(this TranscriptionProvider provider, SettingsManager settingsManager) => IsTrustedProviderId(provider.Id, settingsManager);
private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager) private static bool IsTrustedProviderId(string providerId, SettingsManager settingsManager)
{ {
@@ -5,6 +5,9 @@ using AIStudio.Provider.Reasoning;
namespace AIStudio.Settings; namespace AIStudio.Settings;
public static partial class ProviderExtensions public static partial class ProviderExtensions
{
/// <param name="provider">The configured provider.</param>
extension(Provider provider)
{ {
/// <summary> /// <summary>
/// Get the effective reasoning indicator state for the configured provider instance. /// Get the effective reasoning indicator state for the configured provider instance.
@@ -15,9 +18,8 @@ public static partial class ProviderExtensions
/// own provider. A model which thinks unless told otherwise stops showing the indicator when a /// 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. /// parameter turns it off, and a model which can be asked to think shows it only once one does.
/// </remarks> /// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The effective reasoning indicator state.</returns> /// <returns>The effective reasoning indicator state.</returns>
public static ReasoningIndicatorState GetReasoningIndicatorState(this Provider provider) public ReasoningIndicatorState GetReasoningIndicatorState()
{ {
var reasoning = provider.GetModelProfile().Reasoning; var reasoning = provider.GetModelProfile().Reasoning;
if (reasoning is ReasoningSupport.ALWAYS) if (reasoning is ReasoningSupport.ALWAYS)
@@ -41,3 +43,4 @@ public static partial class ProviderExtensions
return ReasoningIndicatorState.NONE; return ReasoningIndicatorState.NONE;
} }
} }
}
@@ -6,6 +6,9 @@ using AIStudio.Provider;
namespace AIStudio.Settings; namespace AIStudio.Settings;
public static partial class ProviderExtensions public static partial class ProviderExtensions
{
/// <param name="provider">The configured provider.</param>
extension(Provider provider)
{ {
/// <summary> /// <summary>
/// Everything the app knows about the model this provider instance is configured with. /// Everything the app knows about the model this provider instance is configured with.
@@ -16,9 +19,8 @@ public static partial class ProviderExtensions
/// reported, then what the rules worked out from the name, and last what the app assumes when /// reported, then what the rules worked out from the name, and last what the app assumes when
/// nothing else said anything. /// nothing else said anything.
/// </remarks> /// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model.</returns> /// <returns>The profile of the configured model.</returns>
public static ModelProfile GetModelProfile(this Provider provider) public ModelProfile GetModelProfile()
{ {
var automatic = provider.GetAutomaticModelProfile(); var automatic = provider.GetAutomaticModelProfile();
return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic; return provider.CapabilityOverrides?.ApplyTo(automatic) ?? automatic;
@@ -33,46 +35,13 @@ public static partial class ProviderExtensions
/// the window empty gets the number their own engine stated, and a placeholder showing them a /// 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. /// different one would be a promise the app does not keep.
/// </remarks> /// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns>The profile of the configured model, without that provider's overrides.</returns> /// <returns>The profile of the configured model, without that provider's overrides.</returns>
public static ModelProfile GetAutomaticModelProfile(this Provider provider) public ModelProfile GetAutomaticModelProfile()
{ {
var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model); var stated = provider.UsedLLMProvider.GetModelProfile(provider.Model);
return ListedModels.Shared.Of(provider.Id, provider.Model.Id).ApplyTo(stated); 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="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 static ModelProfile GetModelProfile(this LLMProviders provider, 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);
return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated;
}
/// <summary> /// <summary>
/// Get whether the model used by the configured provider accepts images as input. /// Get whether the model used by the configured provider accepts images as input.
/// </summary> /// </summary>
@@ -85,14 +54,52 @@ public static partial class ProviderExtensions
/// A limit of zero images takes the capability back. Zero is a real answer, e.g., from an operator /// 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. /// whose engine accepts no images at all, see ImageLimits; an unknown limit says nothing.
/// </remarks> /// </remarks>
/// <param name="provider">The configured provider.</param>
/// <returns><c>true</c> when the model accepts image input.</returns> /// <returns><c>true</c> when the model accepts image input.</returns>
public static bool SupportsImageInput(this Provider provider) public bool SupportsImageInput()
{ {
var profile = provider.GetModelProfile(); var profile = provider.GetModelProfile();
return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0; return profile.HasAny(Capability.SINGLE_IMAGE_INPUT | Capability.MULTIPLE_IMAGE_INPUT) && profile.Images.MaxInOneMessage is not 0;
} }
}
/// <param name="provider">The LLM provider the model is reached through.</param>
extension(LLMProviders provider)
{
/// <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="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);
return stated.Capabilities is Capability.NONE
? stated with { Capabilities = ModelProfile.ASSUMED.Capabilities }
: stated;
}
}
/// <param name="model">The model to check.</param>
extension(Model model)
{
/// <summary> /// <summary>
/// Checks whether this model can be used for chatting. /// Checks whether this model can be used for chatting.
/// </summary> /// </summary>
@@ -107,32 +114,29 @@ public static partial class ProviderExtensions
/// chat model, so a provider adding a family we have never seen keeps it visible to the person /// 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. /// paying for it. Getting it wrong the other way would hide a model.
/// </remarks> /// </remarks>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</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> /// <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; public bool IsChatModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.CHAT;
/// <summary> /// <summary>
/// Checks whether this model creates embeddings. /// Checks whether this model creates embeddings.
/// </summary> /// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param> /// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an embedding model.</returns> /// <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; public bool IsEmbeddingModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.EMBEDDING;
/// <summary> /// <summary>
/// Checks whether this model transcribes audio. /// Checks whether this model transcribes audio.
/// </summary> /// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param> /// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is a transcription model.</returns> /// <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; public bool IsTranscriptionModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.TRANSCRIPTION;
/// <summary> /// <summary>
/// Checks whether this model generates images. /// Checks whether this model generates images.
/// </summary> /// </summary>
/// <param name="model">The model to check.</param>
/// <param name="provider">The provider serving it.</param> /// <param name="provider">The provider serving it.</param>
/// <returns>True, when the model is an image generation model.</returns> /// <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; public bool IsImageModel(LLMProviders provider) => provider.GetModelProfile(model).Kind is ModelKind.IMAGE_GENERATION;
}
} }
@@ -6,7 +6,9 @@ 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)
{
public string Name() => category switch
{ {
AssistantCategory.AS_IS => TB("Select the assistant category"), AssistantCategory.AS_IS => TB("Select the assistant category"),
AssistantCategory.GENERAL => TB("General"), AssistantCategory.GENERAL => TB("General"),
@@ -21,7 +23,7 @@ public static class AssistantCategoryExtensions
_ => 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");
@@ -29,3 +31,4 @@ public static class AssistantCategoryExtensions
return category.Name(); return category.Name();
} }
} }
}
@@ -10,16 +10,18 @@ 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));
/// <param name="settingsManager">The settings manager to check configuration against.</param>
extension(SettingsManager settingsManager)
{
/// <summary> /// <summary>
/// Checks if an assistant should be visible based on configuration and optional preview feature requirements. /// Checks if an assistant should be visible based on configuration and optional preview feature requirements.
/// </summary> /// </summary>
/// <param name="settingsManager">The settings manager to check configuration against.</param>
/// <param name="withLogging">Whether to log visibility decisions.</param> /// <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="assistantName">The name of the assistant to check (for logging purposes).</param>
/// <param name="component">The assistant component to check.</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> /// <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> /// <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) public bool IsAssistantVisible(Components component, bool withLogging = true, string assistantName = "", PreviewFeatures requiredPreviewFeature = PreviewFeatures.NONE)
{ {
withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName); withLogging = withLogging && !string.IsNullOrWhiteSpace(assistantName);
@@ -93,3 +95,4 @@ public static class AssistantVisibilityExtensions
return !isHidden; return !isHidden;
} }
} }
}
@@ -7,7 +7,9 @@ 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)
{
public string DisplayName() => authMethod switch
{ {
AuthMethod.NONE => TB("None"), AuthMethod.NONE => TB("None"),
AuthMethod.USERNAME_PASSWORD => TB("Username & Password"), AuthMethod.USERNAME_PASSWORD => TB("Username & Password"),
@@ -17,3 +19,4 @@ public static class AuthMethodsV1Extensions
_ => 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)
{
public IEnumerable<ChatTemplate> GetAllChatTemplates()
{ {
yield return ChatTemplate.NO_CHAT_TEMPLATE; yield return ChatTemplate.NO_CHAT_TEMPLATE;
foreach (var chatTemplate in chatTemplates) foreach (var chatTemplate in chatTemplates)
yield return chatTemplate; yield return chatTemplate;
} }
} }
}
@@ -1,14 +1,16 @@
namespace AIStudio.Tools.CodeHighlighting; namespace AIStudio.Tools.CodeHighlighting;
public static class CodeTokenKindExtensions public static class CodeTokenKindExtensions
{
/// <param name="kind">What the part is.</param>
extension(CodeTokenKind kind)
{ {
/// <summary> /// <summary>
/// The CSS class which MudBlazor.Markdown gives this kind of part, so that it takes its color from /// 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. /// the palette of the theme, just as the languages the library highlights itself.
/// </summary> /// </summary>
/// <param name="kind">What the part is.</param>
/// <returns>The CSS class of the part.</returns> /// <returns>The CSS class of the part.</returns>
public static string ToCssClass(this CodeTokenKind kind) => kind switch public string ToCssClass() => kind switch
{ {
CodeTokenKind.KEYWORD => "hljs-keyword", CodeTokenKind.KEYWORD => "hljs-keyword",
CodeTokenKind.TYPE => "hljs-type", CodeTokenKind.TYPE => "hljs-type",
@@ -25,3 +27,4 @@ public static class CodeTokenKindExtensions
_ => string.Empty, _ => string.Empty,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Name() => language switch
{ {
CommonLanguages.AS_IS => TB("Do not change the language"), CommonLanguages.AS_IS => TB("Do not change the language"),
@@ -25,7 +27,7 @@ public static class CommonLanguageExtensions
_ => 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,
@@ -44,7 +46,7 @@ public static class CommonLanguageExtensions
_ => 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}.",
@@ -52,14 +54,14 @@ public static class CommonLanguageExtensions
_ => $"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.",
@@ -67,7 +69,7 @@ public static class CommonLanguageExtensions
_ => $"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");
@@ -75,7 +77,7 @@ public static class CommonLanguageExtensions
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");
@@ -83,3 +85,4 @@ public static class CommonLanguageExtensions
return language.Name(); return language.Name();
} }
} }
}
@@ -10,14 +10,16 @@ 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));
/// <param name="component">The component to look up.</param>
extension(Components component)
{
/// <summary> /// <summary>
/// Gets the preview feature a component belongs to. Components that are generally available /// 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 /// 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. /// its preview feature, so visibility checks never need to special-case one assistant.
/// </summary> /// </summary>
/// <param name="component">The component to look up.</param>
/// <returns>The required preview feature.</returns> /// <returns>The required preview feature.</returns>
public static PreviewFeatures RequiredPreviewFeature(this Components component) => component switch public PreviewFeatures RequiredPreviewFeature() => component switch
{ {
Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026, Components.VISUAL_BRIEFING_ASSISTANT => PreviewFeatures.PRE_VISUAL_BRIEFING_ASSISTANT_2026,
@@ -35,9 +37,8 @@ public static class ComponentsExtensions
/// component. All must be excluded from the single-slot checks, which is why this is a /// component. All must be excluded from the single-slot checks, which is why this is a
/// capability and not a component comparison. /// capability and not a component comparison.
/// </remarks> /// </remarks>
/// <param name="component">The component to look up.</param>
/// <returns><c>true</c> when the component owns exactly one session slot.</returns> /// <returns><c>true</c> when the component owns exactly one session slot.</returns>
public static bool HasSingleSessionSlot(this Components component) => component switch public bool HasSingleSessionSlot() => component switch
{ {
Components.CHAT or Components.DYNAMIC_ASSISTANT => false, Components.CHAT or Components.DYNAMIC_ASSISTANT => false,
Components.VISUAL_BRIEFING_ASSISTANT => false, Components.VISUAL_BRIEFING_ASSISTANT => false,
@@ -48,16 +49,15 @@ public static class ComponentsExtensions
/// <summary> /// <summary>
/// Gets the kind of media-import owner a component creates for its attachments. /// Gets the kind of media-import owner a component creates for its attachments.
/// </summary> /// </summary>
/// <param name="component">The component to look up.</param>
/// <returns>The media-import owner kind.</returns> /// <returns>The media-import owner kind.</returns>
public static MediaImportOwnerKind MediaOwnerKind(this Components component) => component switch public MediaImportOwnerKind MediaOwnerKind() => component switch
{ {
Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING, Components.VISUAL_BRIEFING_ASSISTANT => MediaImportOwnerKind.VISUAL_BRIEFING,
_ => MediaImportOwnerKind.ASSISTANT, _ => MediaImportOwnerKind.ASSISTANT,
}; };
public static bool AllowSendTo(this Components component) => component switch public bool AllowSendTo() => component switch
{ {
Components.NONE => false, Components.NONE => false,
Components.DYNAMIC_ASSISTANT => false, Components.DYNAMIC_ASSISTANT => false,
@@ -81,7 +81,7 @@ public static class ComponentsExtensions
_ => true, _ => true,
}; };
public static string Name(this Components component) => component switch public string Name() => component switch
{ {
Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"), Components.GRAMMAR_SPELLING_ASSISTANT => TB("Grammar & Spelling Assistant"),
Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"), Components.TEXT_SUMMARIZER_ASSISTANT => TB("Text Summarizer Assistant"),
@@ -111,7 +111,7 @@ public static class ComponentsExtensions
_ => Enum.GetName(component)!, _ => Enum.GetName(component)!,
}; };
public static ComponentsData GetData(this Components destination) => destination switch public ComponentsData GetData() => component switch
{ {
Components.AGENDA_ASSISTANT => new(Event.SEND_TO_AGENDA_ASSISTANT, Routes.ASSISTANT_AGENDA), 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.CODING_ASSISTANT => new(Event.SEND_TO_CODING_ASSISTANT, Routes.ASSISTANT_CODING),
@@ -136,7 +136,7 @@ public static class ComponentsExtensions
_ => new(Event.NONE, Routes.ASSISTANTS), _ => new(Event.NONE, Routes.ASSISTANTS),
}; };
public static ConfidenceLevel MinimumConfidence(this Components component, SettingsManager settingsManager) => component switch public ConfidenceLevel MinimumConfidence(SettingsManager settingsManager) => component switch
{ {
Components.GRAMMAR_SPELLING_ASSISTANT => settingsManager.ConfigurationData.GrammarSpelling.PreselectOptions ? settingsManager.ConfigurationData.GrammarSpelling.MinimumProviderConfidence : default, 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.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.ConfigurationData.IconFinder.MinimumProviderConfidence : default,
@@ -169,7 +169,7 @@ public static class ComponentsExtensions
_ => default, _ => default,
}; };
public static AIStudio.Settings.Provider PreselectedProvider(this Components component, SettingsManager settingsManager) => component switch 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.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.ICON_FINDER_ASSISTANT => settingsManager.ConfigurationData.IconFinder.PreselectOptions ? settingsManager.GetProviderById(settingsManager.ConfigurationData.IconFinder.PreselectedProvider) : Settings.Provider.NONE,
@@ -209,7 +209,7 @@ public static class ComponentsExtensions
_ => Settings.Provider.NONE, _ => Settings.Provider.NONE,
}; };
public static ProfilePreselection GetProfilePreselection(this Components component, SettingsManager settingsManager) public ProfilePreselection GetProfilePreselection(SettingsManager settingsManager)
{ {
var storedValue = component switch var storedValue = component switch
{ {
@@ -235,7 +235,7 @@ public static class ComponentsExtensions
return ProfilePreselection.FromStoredValue(storedValue); return ProfilePreselection.FromStoredValue(storedValue);
} }
public static ChatTemplate PreselectedChatTemplate(this Components component, SettingsManager settingsManager) => component switch public ChatTemplate PreselectedChatTemplate(SettingsManager settingsManager) => component switch
{ {
// Dynamic assistants have no dedicated settings yet, so they derive their defaults from the chat: // 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, Components.DYNAMIC_ASSISTANT or Components.CHAT => settingsManager.ConfigurationData.Chat.PreselectOptions ? settingsManager.GetChatTemplateById(settingsManager.ConfigurationData.Chat.PreselectedChatTemplate) : ChatTemplate.NO_CHAT_TEMPLATE,
@@ -243,3 +243,4 @@ public static class ComponentsExtensions
_ => ChatTemplate.NO_CHAT_TEMPLATE, _ => ChatTemplate.NO_CHAT_TEMPLATE,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string GetListDescription() => scheme switch
{ {
ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"), ConfidenceSchemes.TRUST_ALL => TB("Trust all LLM providers"),
ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"), ConfidenceSchemes.TRUST_USA_EUROPE => TB("Trust LLM providers from the USA and Europe"),
@@ -20,3 +22,4 @@ public static class ConfidenceSchemesExtensions
_ => TB("Unknown confidence scheme"), _ => TB("Unknown confidence scheme"),
}; };
} }
}
@@ -1,6 +1,9 @@
namespace AIStudio.Tools; namespace AIStudio.Tools;
public static class DirectoryInfoExtensions public static class DirectoryInfoExtensions
{
/// <param name="directoryInfo">The root directory to determine the size of.</param>
extension(DirectoryInfo directoryInfo)
{ {
/// <summary> /// <summary>
/// Determines the size and file count of the files a selection takes from a directory. When /// Determines the size and file count of the files a selection takes from a directory. When
@@ -17,7 +20,6 @@ public static class DirectoryInfoExtensions
/// current total size, you need to use the appropriate dispatcher to update the UI. Usually, you can use the InvokeAsync /// 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. /// method to update the UI from a background thread.
/// </remarks> /// </remarks>
/// <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> /// <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="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="reportNextFile">The callback to report the next file found. The file name is relative to the root directory.</param>
@@ -25,7 +27,7 @@ public static class DirectoryInfoExtensions
/// <param name="done">The callback to report that the operation is done.</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="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> /// <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) 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 rootDirectoryLen = directoryInfo.FullName.Length;
var filesToCount = selection ?? FolderFileSelection.EVERY_FILE; var filesToCount = selection ?? FolderFileSelection.EVERY_FILE;
@@ -65,3 +67,4 @@ public static class DirectoryInfoExtensions
done?.Invoke(); done?.Invoke();
} }
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string Explain() => providerType switch
{ {
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.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.ANY => TB("The related data can be sent to any provider, regardless of where it is hosted (cloud or self-hosted)."),
@@ -15,3 +17,4 @@ public static class ProviderTypeExtensions
_ => 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)
{
public async ValueTask ScrollIntoViewAsync(IJSRuntime jsRuntime)
{ {
await jsRuntime.InvokeVoidAsync("scrollToBottom", elementReference); 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 static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText); public async Task<EncryptedText> Encrypt(Encryption encryption) => await encryption.Encrypt(data);
}
extension(EncryptedText encryptedText)
{
public async Task<string> Decrypt(Encryption encryption) => await encryption.Decrypt(encryptedText);
}
} }
@@ -8,16 +8,18 @@ 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>
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression)
{
/// <summary>
/// Extracts the member expression from a given lambda expression representing a property.
/// </summary>
/// <returns>The member expression that represents the property access.</returns> /// <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> /// <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) public MemberExpression GetMemberExpression()
{ {
switch (expression.Body) switch (expression.Body)
{ {
@@ -34,19 +36,22 @@ public static class ExpressionExtensions
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));
} }
} }
}
/// <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>
/// <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>
extension<TIn, TOut>(Expression<Func<TIn, TOut>> expression) where TOut : IBinaryInteger<TOut>
{
/// <summary> /// <summary>
/// Attempts to increment the value of an uint property for a specified object using a /// Attempts to increment the value of an uint property for a specified object using a
/// provided expression. /// provided expression.
/// </summary> /// </summary>
/// <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>
/// <param name="data">The object that contains the property referenced by the expression.</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> /// <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="TOut">The type of the property to be incremented.</typeparam>
/// <returns>An IncrementResult object containing the result of the increment operation.</returns> /// <returns>An IncrementResult object containing the result of the increment operation.</returns>
public static IncrementResult<TOut> TryIncrement<TIn, TOut>(this Expression<Func<TIn, TOut>> expression, TIn data, IncrementType type) where TOut : IBinaryInteger<TOut> public IncrementResult<TOut> TryIncrement(TIn data, IncrementType type)
{ {
// Ensure that the expression body is a member expression: // Ensure that the expression body is a member expression:
if (expression.Body is not MemberExpression memberExpression) if (expression.Body is not MemberExpression memberExpression)
@@ -94,3 +99,4 @@ public static class ExpressionExtensions
} }
} }
} }
}
@@ -52,12 +52,14 @@ 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];
/// <param name="format">The format.</param>
extension(FileExportFormat format)
{
/// <summary> /// <summary>
/// Returns the name of the format as shown to the user. /// Returns the name of the format as shown to the user.
/// </summary> /// </summary>
/// <param name="format">The format.</param>
/// <returns>The name of the format.</returns> /// <returns>The name of the format.</returns>
public static string ToName(this FileExportFormat format) => format switch public string ToName() => format switch
{ {
FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"), FileExportFormat.MICROSOFT_WORD => TB("Microsoft Word (.docx)"),
FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"), FileExportFormat.OPEN_DOCUMENT_TEXT => TB("OpenDocument Text (.odt), e.g. LibreOffice"),
@@ -74,9 +76,8 @@ public static class FileExportFormatExtensions
/// <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 static string ToIcon(this FileExportFormat format) => format switch public string ToIcon() => 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,
@@ -92,10 +93,9 @@ public static class FileExportFormatExtensions
/// <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 static string ToFileExtension(this FileExportFormat format) => format switch public string ToFileExtension() => format switch
{ {
FileExportFormat.MICROSOFT_WORD => ".docx", FileExportFormat.MICROSOFT_WORD => ".docx",
FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt", FileExportFormat.OPEN_DOCUMENT_TEXT => ".odt",
@@ -108,6 +108,160 @@ public static class FileExportFormatExtensions
_ => 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
/// code block. /// code block.
@@ -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,13 +15,15 @@ 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));
/// <param name="code">The stable failure code.</param>
extension(FileExtractionErrorCode code)
{
/// <summary> /// <summary>
/// Gets a value indicating whether reading the file again will fail again, as long as the file /// Gets a value indicating whether reading the file again will fail again, as long as the file
/// itself does not change. /// itself does not change.
/// </summary> /// </summary>
/// <param name="code">The stable failure code.</param>
/// <returns>True, when the reason lies in the file itself.</returns> /// <returns>True, when the reason lies in the file itself.</returns>
internal static bool IsPermanentIndexingFailure(this FileExtractionErrorCode code) => code switch internal bool IsPermanentIndexingFailure() => code switch
{ {
// //
// The reason lies in the file. Reading it again without changing it produces the same // The reason lies in the file. Reading it again without changing it produces the same
@@ -58,9 +60,8 @@ internal static class FileExtractionErrorCodeExtensions
/// 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 static string GetIndexingCauseName(this FileExtractionErrorCode code) => code switch internal string GetIndexingCauseName() => 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"),
@@ -103,10 +104,10 @@ internal static class FileExtractionErrorCodeExtensions
/// 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 static string ToIndexingUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToIndexingMessageFormat(code), fileName); internal string ToIndexingUserMessage(string fileName) => string.Format(ToIndexingMessageFormat(code), fileName);
}
private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch private static string ToIndexingMessageFormat(FileExtractionErrorCode code) => code switch
{ {
@@ -14,13 +14,15 @@ 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));
/// <param name="result">The extraction result.</param>
extension(FileExtractionResult result)
{
/// <summary> /// <summary>
/// Gets the localized message which explains why a file could not be read. /// Gets the localized message which explains why a file could not be read.
/// </summary> /// </summary>
/// <param name="result">The extraction result.</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 static string ToUserMessage(this FileExtractionResult result, string fileName) internal string ToUserMessage(string fileName)
{ {
// When we know what the file really is, naming it beats a generic "not supported": // 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) if (result.ErrorCode is FileExtractionErrorCode.UNSUPPORTED && result.DetectedFormat is not null)
@@ -36,14 +38,30 @@ internal static class FileExtractionResultExtensions
/// This is a notice, not a failure: the file was read according to its content. We still tell /// 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. /// the user, because a wrong extension is a real problem for every other program as well.
/// </remarks> /// </remarks>
/// <param name="result">The extraction result.</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 static string ToExtensionMismatchUserMessage(this FileExtractionResult result, string fileName) => string.Format( 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."), TB("The file '{0}' is actually a {1} and was read as such. Please correct its file extension."),
fileName, fileName,
result.DetectedFormat); 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));
}
}
/// <param name="code">The stable failure code.</param>
extension(FileExtractionErrorCode code)
{
/// <summary> /// <summary>
/// Gets the localized message which explains why a file could not be read. /// Gets the localized message which explains why a file could not be read.
/// </summary> /// </summary>
@@ -51,23 +69,9 @@ internal static class FileExtractionResultExtensions
/// This overload exists for the places which know the reason before an extraction was even /// 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. /// attempted, so both ways of skipping a file tell the user the same thing.
/// </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 static string ToUserMessage(this FileExtractionErrorCode code, string fileName) => string.Format(ToUserMessageFormat(code), fileName); internal string ToUserMessage(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)
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));
} }
private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch private static string ToUserMessageFormat(FileExtractionErrorCode code) => code switch
@@ -1,13 +1,15 @@
namespace AIStudio.Tools; namespace AIStudio.Tools;
public static class FileInfoExtensions public static class FileInfoExtensions
{
/// <param name="fileInfo">The file info object.</param>
extension(FileInfo fileInfo)
{ {
/// <summary> /// <summary>
/// Returns the file size in human-readable format. /// Returns the file size in human-readable format.
/// </summary> /// </summary>
/// <param name="fileInfo">The file info object.</param>
/// <returns>The file size in human-readable format.</returns> /// <returns>The file size in human-readable format.</returns>
public static string FileSize(this FileInfo fileInfo) public string FileSize()
{ {
if (!fileInfo.Exists) if (!fileInfo.Exists)
return "N/A"; return "N/A";
@@ -15,3 +17,4 @@ public static class FileInfoExtensions
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)
{
public void AddApiToken()
{ {
headers.Add("token", API_TOKEN); headers.Add("token", API_TOKEN);
} }
} }
}
@@ -4,7 +4,11 @@ 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 /// <param name="items">The list of confidence items to analyze.</param>
/// <typeparam name="T">The type of items in the list.</typeparam>
extension<T>(IReadOnlyList<T> items) where T : IConfidence
{
public TargetWindow DetermineTargetWindow(TargetWindowStrategy strategy, int numMaximumItems = 30)
{ {
switch (strategy) switch (strategy)
{ {
@@ -46,12 +50,10 @@ public static class IConfidenceExtensions
/// Determine the optimal confidence threshold for a list of items /// Determine the optimal confidence threshold for a list of items
/// in order to match a target window of number of items. /// in order to match a target window of number of items.
/// </summary> /// </summary>
/// <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="targetWindow">The target window for the number of items.</param>
/// <param name="maxSteps">The maximum number of steps to search for the threshold.</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>
/// <returns>The confidence threshold.</returns> /// <returns>The confidence threshold.</returns>
public static float GetConfidenceThreshold<T>(this IReadOnlyList<T> items, TargetWindow targetWindow, int maxSteps = 10) where T : IConfidence public float GetConfidenceThreshold(TargetWindow targetWindow, int maxSteps = 10)
{ {
if(!targetWindow.IsValid()) if(!targetWindow.IsValid())
{ {
@@ -108,3 +110,4 @@ public static class IConfidenceExtensions
return threshold; return threshold;
} }
} }
}
@@ -3,19 +3,28 @@ 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)
{
public async Task GenerateAndShowDiff(string text1, string text2)
{ {
await jsRuntime.InvokeVoidAsync("generateDiff", text1, text2, AssistantLowerBase.RESULT_DIV_ID, AssistantLowerBase.BEFORE_RESULT_DIV_ID); 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>
/// Calls a JavaScript function which returns nothing, and tolerates a circuit which is already gone. /// Calls a JavaScript function which returns nothing, and tolerates a circuit which is already gone.
@@ -3,6 +3,9 @@ using System.Text.Json;
namespace AIStudio.Tools; namespace AIStudio.Tools;
public static class JsonSerializerOptionsExtensions public static class JsonSerializerOptionsExtensions
{
/// <param name="options">The options, with their TypeInfoResolver set.</param>
extension(JsonSerializerOptions options)
{ {
/// <summary> /// <summary>
/// Makes the options read-only and returns them, so that a static field gets its options in one expression. /// Makes the options read-only and returns them, so that a static field gets its options in one expression.
@@ -11,11 +14,11 @@ public static class JsonSerializerOptionsExtensions
/// Options with contracts of their own, i.e., with a TypeInfoResolver, should never change after they were /// 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. /// declared: whoever used them before such a change would have read or written with other contracts.
/// </remarks> /// </remarks>
/// <param name="options">The options, with their TypeInfoResolver set.</param>
/// <returns>The same options, now read-only.</returns> /// <returns>The same options, now read-only.</returns>
public static JsonSerializerOptions AsReadOnly(this JsonSerializerOptions options) public JsonSerializerOptions AsReadOnly()
{ {
options.MakeReadOnly(); options.MakeReadOnly();
return options; return options;
} }
} }
}
+16 -8
View File
@@ -8,6 +8,9 @@ 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"];
/// <param name="count">The number to format.</param>
extension(long count)
{
/// <summary> /// <summary>
/// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M. /// Formats a count so that large numbers stay readable: 1456 becomes 1.46k, 4512900 becomes 4.51M.
/// </summary> /// </summary>
@@ -17,9 +20,8 @@ public static class LongExtensions
/// difference between 4 and 999. The decimal separator follows the culture of the active language /// 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. /// plugin rather than the one of the thread, which never moves along with the app's language.
/// </remarks> /// </remarks>
/// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns> /// <returns>The formatted number.</returns>
public static string CompactCount(this long count) public string CompactCount()
{ {
var culture = I18N.I.Culture; var culture = I18N.I.Culture;
if (count is > -1_000 and < 1_000) if (count is > -1_000 and < 1_000)
@@ -35,31 +37,36 @@ public static class LongExtensions
return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}"; return $"{value.ToString("0.##", culture)}{COUNT_SUFFIXES[order]}";
} }
}
/// <param name="count">The number to format.</param>
extension(int count)
{
/// <summary> /// <summary>
/// Formats a count so that large numbers stay readable. /// Formats a count so that large numbers stay readable.
/// </summary> /// </summary>
/// <param name="count">The number to format.</param>
/// <returns>The formatted number.</returns> /// <returns>The formatted number.</returns>
public static string CompactCount(this int count) => ((long)count).CompactCount(); public string CompactCount() => ((long)count).CompactCount();
}
/// <param name="sizeBytes">The size in bytes.</param>
extension(long sizeBytes)
{
/// <summary> /// <summary>
/// Formats the file size in a human-readable format, e.g., 1.9 MB. /// Formats the file size in a human-readable format, e.g., 1.9 MB.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Like CompactCount, the decimal separator follows the culture of the active language plugin. /// Like CompactCount, the decimal separator follows the culture of the active language plugin.
/// </remarks> /// </remarks>
/// <param name="sizeBytes">The size in bytes.</param>
/// <returns>The formatted file size.</returns> /// <returns>The formatted file size.</returns>
public static string FileSize(this long sizeBytes) => sizeBytes.FileSize(I18N.I.Culture); public string FileSize() => sizeBytes.FileSize(I18N.I.Culture);
/// <summary> /// <summary>
/// Formats the file size in a human-readable format, with the decimal separator of the given culture. /// Formats the file size in a human-readable format, with the decimal separator of the given culture.
/// </summary> /// </summary>
/// <param name="sizeBytes">The size in bytes.</param>
/// <param name="culture">The culture whose decimal separator to use.</param> /// <param name="culture">The culture whose decimal separator to use.</param>
/// <returns>The formatted file size.</returns> /// <returns>The formatted file size.</returns>
internal static string FileSize(this long sizeBytes, IFormatProvider culture) internal string FileSize(IFormatProvider culture)
{ {
var order = 0; var order = 0;
double size = sizeBytes; double size = sizeBytes;
@@ -72,3 +79,4 @@ public static class LongExtensions
return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}"; return $"{size.ToString("0.##", culture)} {SIZE_UNITS[order]}";
} }
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Tools.MIME;
public static class MIMETypeExtensions public static class MIMETypeExtensions
{ {
public static string[] ToStringArray(this MIMEType[] mimeTypes) extension(MIMEType[] mimeTypes)
{
public string[] ToStringArray()
{ {
var result = new string[mimeTypes.Length]; var result = new string[mimeTypes.Length];
for (var i = 0; i < mimeTypes.Length; i++) for (var i = 0; i < mimeTypes.Length; i++)
@@ -13,3 +15,4 @@ public static class MIMETypeExtensions
return result; return result;
} }
} }
}
@@ -6,10 +6,12 @@ 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));
extension(MailboxConnectionFailure failure)
{
/// <summary> /// <summary>
/// What went wrong, and what the user can do about it. /// What went wrong, and what the user can do about it.
/// </summary> /// </summary>
public static string GetDescription(this MailboxConnectionFailure failure) => failure switch public string GetDescription() => 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."), 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.NETWORK_UNAVAILABLE => TB("The server could not be reached. Please check the host and the port, and whether you need a VPN connection."),
@@ -21,3 +23,4 @@ public static class MailboxConnectionFailureExtensions
_ => TB("The connection to the server failed for an unknown reason."), _ => TB("The connection to the server failed for an unknown reason."),
}; };
} }
}
@@ -6,10 +6,12 @@ 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));
extension(MailboxUsernameFormat format)
{
/// <summary> /// <summary>
/// What the user enters as the username at this provider. /// What the user enters as the username at this provider.
/// </summary> /// </summary>
public static string GetDescription(this MailboxUsernameFormat format) => format switch public string GetDescription() => format switch
{ {
MailboxUsernameFormat.EMAIL_ADDRESS => TB("Your full e-mail address."), 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.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."),
@@ -17,11 +19,14 @@ public static class MailboxProviderTemplateExtensions
_ => TB("The username your provider gave you."), _ => TB("The username your provider gave you."),
}; };
}
extension(MailboxProviderRequirements requirements)
{
/// <summary> /// <summary>
/// What the user has to do before AI Studio can sign in, one text per requirement. /// What the user has to do before AI Studio can sign in, one text per requirement.
/// </summary> /// </summary>
public static IEnumerable<string> GetDescriptions(this MailboxProviderRequirements requirements) public IEnumerable<string> GetDescriptions()
{ {
if (requirements.HasFlag(MailboxProviderRequirements.IMAP_ACTIVATION)) 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."); 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.");
@@ -36,3 +41,4 @@ public static class MailboxProviderTemplateExtensions
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.");
} }
} }
}
@@ -2,6 +2,9 @@ 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
{
/// <param name="kind">The owner kind to look up.</param>
extension(MediaImportOwnerKind kind)
{ {
/// <summary> /// <summary>
/// Gets whether the owner stores its own source list and transcripts. /// Gets whether the owner stores its own source list and transcripts.
@@ -13,7 +16,7 @@ public static class MediaImportOwnerKindExtensions
/// the file, nor deliver the completed transcripts back into its own list afterwards, because /// 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. /// the owner already holds them. All other owners rely on that delivery instead.
/// </remarks> /// </remarks>
/// <param name="kind">The owner kind to look up.</param>
/// <returns><c>true</c> when the owner persists its own sources.</returns> /// <returns><c>true</c> when the owner persists its own sources.</returns>
public static bool PersistsOwnSources(this MediaImportOwnerKind kind) => kind is MediaImportOwnerKind.VISUAL_BRIEFING; 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)
{
public async Task SendMessage<T>(Event triggeredEvent, T? data = default)
{ {
await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data); await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data);
} }
}
public static void ApplyFilters(this IMessageBusReceiver component, ComponentBase[] components, Event[] events) extension(IMessageBusReceiver component)
{
public void ApplyFilters(ComponentBase[] components, Event[] events)
{ {
MessageBus.INSTANCE.ApplyFilters(component, components, events.ToHashSet()); 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)
{
public Palette GetCurrentPalette(SettingsManager settingsManager) => settingsManager.IsDarkMode switch
{ {
true => theme.PaletteDark, true => theme.PaletteDark,
false => theme.PaletteLight, 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,7 +4,9 @@ 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)
{
public string GetDisplayName() => type switch
{ {
AssistantComponentType.FORM => TB("Root"), AssistantComponentType.FORM => TB("Root"),
AssistantComponentType.TEXT_AREA => TB("Text Area"), AssistantComponentType.TEXT_AREA => TB("Text Area"),
@@ -34,7 +36,7 @@ public static class AssistantComponentTypeExtensions
_ => TB("Unknown Element") _ => 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,
@@ -64,3 +66,4 @@ public static class AssistantComponentTypeExtensions
_ => MudBlazor.Icons.Material.Filled.AccountTree, _ => MudBlazor.Icons.Material.Filled.AccountTree,
}; };
} }
}
@@ -6,7 +6,9 @@ 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)
{
public string GetText(ILanguagePlugin plugin, string fallbackEN, string? typeNamespace = null, string? typeName = null)
{ {
var type = lang.GetType(); var type = lang.GetType();
typeName ??= type.Name; typeName ??= type.Name;
@@ -34,3 +36,4 @@ public static class ILangExtensions
return fallbackEN; return fallbackEN;
} }
} }
}
@@ -2,7 +2,9 @@ namespace AIStudio.Tools.PluginSystem;
public static class InternalPluginExtensions public static class InternalPluginExtensions
{ {
public static InternalPluginData MetaData(this InternalPlugin plugin) => plugin switch extension(InternalPlugin plugin)
{
public InternalPluginData MetaData() => plugin switch
{ {
InternalPlugin.LANGUAGE_EN_US => new (PluginType.LANGUAGE, new("97dfb1ba-50c4-4440-8dfa-6575daf543c8"), "en-us"), 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"), InternalPlugin.LANGUAGE_DE_DE => new(PluginType.LANGUAGE, new("43065dbc-78d0-45b7-92be-f14c2926e2dc"), "de-de"),
@@ -10,3 +12,4 @@ public static class InternalPluginExtensions
_ => 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