diff --git a/README.md b/README.md index 6a5f850d..81914d68 100644 --- a/README.md +++ b/README.md @@ -214,3 +214,20 @@ MindWork AI Studio is licensed under the `FSL-1.1-MIT` license (functional sourc For more details, refer to the [LICENSE](LICENSE.md) file. This license structure ensures you have plenty of freedom to use and enjoy the software while protecting our work. + +
+ +

+ Trademarks +

+
+ +The license above covers our own software. It says nothing about the trademarks of other companies, so here is where AI Studio stands on those. + +AI Studio ships the logos of the AI providers it supports and shows them next to the matching provider entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies. + +Some of these logos come from the [Simple Icons](https://github.com/simple-icons/simple-icons) project, which publishes them under [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md); the trademarks themselves are not part of that release. The remaining ones were taken from the official brand resources of the respective provider. The source of every single file is documented in [the provider icon notes](app/MindWork%20AI%20Studio/wwwroot/images/provider-icons/README.md). All logos ship with AI Studio and are loaded from your device, so showing one never sends a request to the provider. + +Organizations can replace these logos with their own icons through a configuration plugin. When an organization does so, it is responsible for holding the rights to the icons it provides. + +
diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a989292f..1ffeda02 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -8320,6 +8320,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1402243995"] = "Updates are mana -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." +-- Trademarks & Brand Assets +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421823619"] = "Trademarks & Brand Assets" + -- Copies the allowed host pattern to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Copies the allowed host pattern to the clipboard" @@ -8329,6 +8332,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the -- Encryption secret: is not configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secret: is not configured" +-- Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T158845920"] = "Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides." + -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active." @@ -8344,6 +8350,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" +-- Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1699089284"] = "Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners." + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8386,6 +8395,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8650,6 +8662,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provide -- Allowed host: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Allowed host:" +-- Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3775183188"] = "Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider." + -- Configuration source: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:" diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor index be6a93cd..1b1c8e47 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor @@ -1,2 +1,13 @@ @inherits MSGComponentBase - \ No newline at end of file + + + @if (this.GetProvider(providerData.Value) is { } provider) + { + + } + else + { + @providerData.Name + } + + diff --git a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs index 722ad15f..9018afcb 100644 --- a/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationProviderSelection.razor.cs @@ -32,7 +32,7 @@ public partial class ConfigurationProviderSelection : MSGComponentBase [Parameter] public Func IsLocked { get; set; } = () => false; - + private IEnumerable> FilteredData() { if(this.Component is not Tools.Components.NONE and not Tools.Components.APP_SETTINGS) @@ -51,6 +51,15 @@ public partial class ConfigurationProviderSelection : MSGComponentBase } } + private AIStudio.Settings.Provider? GetProvider(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + return null; + + var provider = this.SettingsManager.GetProviderById(providerId); + return provider == AIStudio.Settings.Provider.NONE ? null : provider; + } + #region Overrides of MSGComponentBase protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default diff --git a/app/MindWork AI Studio/Components/ConfigurationSelect.razor b/app/MindWork AI Studio/Components/ConfigurationSelect.razor index c3459101..4d708899 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSelect.razor +++ b/app/MindWork AI Studio/Components/ConfigurationSelect.razor @@ -5,7 +5,14 @@ @foreach (var data in this.Data) { - @data.Name + @if (this.ItemTemplate is null) + { + @data.Name + } + else + { + @this.ItemTemplate(data) + } } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs index 820a4ee0..fa0a51ab 100644 --- a/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs +++ b/app/MindWork AI Studio/Components/ConfigurationSelect.razor.cs @@ -33,7 +33,13 @@ public partial class ConfigurationSelect : ConfigurationBaseCore /// [Parameter] public Func SelectionUpdateAsync { get; set; } = _ => Task.CompletedTask; - + + /// + /// Optional template used to render an item in the list. + /// + [Parameter] + public RenderFragment>? ItemTemplate { get; set; } + #region Overrides of ConfigurationBase /// @@ -54,4 +60,4 @@ public partial class ConfigurationSelect : ConfigurationBaseCore await this.SettingsManager.StoreSettings(); await this.InformAboutChange(); } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Components/ProviderIcon.razor b/app/MindWork AI Studio/Components/ProviderIcon.razor new file mode 100644 index 00000000..3f91cd81 --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderIcon.razor @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderIcon.razor.cs b/app/MindWork AI Studio/Components/ProviderIcon.razor.cs new file mode 100644 index 00000000..3931fe17 --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderIcon.razor.cs @@ -0,0 +1,59 @@ +using AIStudio.Provider; +using AIStudio.Settings; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// Shows the icon of a provider. +/// +/// +/// The icon is rendered as an image instead of inline SVG. That way the browser treats the icon as +/// a standalone, script-less document, which matters for the custom icons a configuration plugin +/// may supply. +/// +public partial class ProviderIcon : ComponentBase +{ + /// + /// The configured provider whose icon should be shown. Takes precedence over ProviderType. + /// + [Parameter] + public AIStudio.Settings.Provider? ProviderSettings { get; set; } + + /// + /// The LLM provider whose icon should be shown when no ProviderSettings was given. + /// + [Parameter] + public LLMProviders ProviderType { get; set; } = LLMProviders.NONE; + + /// + /// The validated custom icon supplied by a configuration plugin. + /// + [Parameter] + public string CustomIconDataUrl { get; set; } = string.Empty; + + /// + /// Additional CSS class for the icon. + /// + [Parameter] + public string Class { get; set; } = string.Empty; + + /// + /// Additional inline style for the icon. + /// + [Parameter] + public string Style { get; set; } = string.Empty; + + [Inject] + private SettingsManager SettingsManager { get; init; } = null!; + + /// + /// The provider-icon class carries the sizing from app.css. Callers add to it instead of + /// replacing it, so an icon cannot lose its size by setting a class of its own. + /// + private string CssClass => $"provider-icon {this.Class}".TrimEnd(); + + private string IconUrl => this.ProviderSettings?.GetIconUrl(this.SettingsManager.IsDarkMode) + ?? this.ProviderType.GetIconUrl(this.SettingsManager.IsDarkMode, this.CustomIconDataUrl); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderLabel.razor b/app/MindWork AI Studio/Components/ProviderLabel.razor new file mode 100644 index 00000000..94ea0208 --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderLabel.razor @@ -0,0 +1,4 @@ + + + @this.Text + \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderLabel.razor.cs b/app/MindWork AI Studio/Components/ProviderLabel.razor.cs new file mode 100644 index 00000000..f78668cb --- /dev/null +++ b/app/MindWork AI Studio/Components/ProviderLabel.razor.cs @@ -0,0 +1,46 @@ +using AIStudio.Provider; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Components; + +/// +/// Shows a provider icon next to its name. +/// +/// +/// Providers appear in select items, in table cells, and in group headers. All of them need the +/// same icon and text pairing, so this component owns that layout once instead of repeating it at +/// every call site. +/// +public partial class ProviderLabel : ComponentBase +{ + /// + /// The configured provider whose icon should be shown. Takes precedence over ProviderType. + /// + [Parameter] + public AIStudio.Settings.Provider? ProviderSettings { get; set; } + + /// + /// The LLM provider whose icon should be shown when no ProviderSettings was given. + /// + [Parameter] + public LLMProviders ProviderType { get; set; } = LLMProviders.NONE; + + /// + /// The validated custom icon supplied by a configuration plugin. + /// + [Parameter] + public string CustomIconDataUrl { get; set; } = string.Empty; + + /// + /// The text shown next to the icon. + /// + [Parameter] + public string Text { get; set; } = string.Empty; + + /// + /// Additional CSS class for the text. + /// + [Parameter] + public string TextClass { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor b/app/MindWork AI Studio/Components/ProviderSelection.razor index 4d5b0887..e4ca191a 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor @@ -1,11 +1,11 @@ @using AIStudio.Settings @inherits MSGComponentBase - + @foreach (var providerItem in this.GetAvailableProviderSelectionItems()) { - @providerItem.Provider + @if (providerItem.CapabilityIcons.Count > 0) { diff --git a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs index 20313116..90b4f460 100644 --- a/app/MindWork AI Studio/Components/ProviderSelection.razor.cs +++ b/app/MindWork AI Studio/Components/ProviderSelection.razor.cs @@ -105,12 +105,24 @@ public partial class ProviderSelection : MSGComponentBase #region Overrides of MSGComponentBase - protected override Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default + protected override async Task ProcessIncomingMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default { if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED) - this.StateHasChanged(); + { + // + // We hold a copy of the provider record, which is a snapshot taken when it was selected. + // Once the user edits that provider, our copy is stale and would keep showing the old + // name and the old icon, so we resolve it again and hand the fresh one to our parent: + // + var updatedProvider = this.SettingsManager.GetProviderById(this.ProviderSettings.Id); + if (updatedProvider != AIStudio.Settings.Provider.NONE && updatedProvider != this.ProviderSettings) + { + this.ProviderSettings = updatedProvider; + await this.ProviderSettingsChanged.InvokeAsync(updatedProvider); + } - return Task.CompletedTask; + this.StateHasChanged(); + } } #endregion @@ -118,4 +130,4 @@ public partial class ProviderSelection : MSGComponentBase private readonly record struct CapabilityIcon(string Icon, string Tooltip); private readonly record struct ProviderSelectionItem(AIStudio.Settings.Provider Provider, IReadOnlyList CapabilityIcons); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor index 3b7a4ce4..071b9cbc 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor @@ -37,7 +37,18 @@ @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) { - + + + @if (this.GetTranscriptionProvider(providerData.Value) is { } provider) + { + + } + else + { + @providerData.Name + } + + } diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs index ac5d07a0..d0a5a368 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelApp.razor.cs @@ -106,6 +106,12 @@ public partial class SettingsPanelApp : SettingsPanelBase } } + private TranscriptionProvider? GetTranscriptionProvider(string providerId) + { + var provider = this.SettingsManager.GetTranscriptionProviderById(providerId); + return provider == TranscriptionProvider.NONE ? null : provider; + } + private void UpdatePreviewFeatures(PreviewVisibility previewVisibility) { this.SettingsManager.ConfigurationData.App.PreviewVisibility = previewVisibility; diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor index a4ad15f7..1de2dd17 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor @@ -30,11 +30,16 @@ - @context.Key + @if (context.Key is LLMProviders llmProvider) + { + + } - @context.Name + + + @this.GetEmbeddingProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs index dd74f949..ba0f4587 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelEmbeddings.razor.cs @@ -19,7 +19,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { Expandable = true, IsInitiallyExpanded = false, - Selector = provider => provider.UsedLLMProvider.ToName(), + Selector = provider => provider.UsedLLMProvider, }; [Parameter] @@ -82,6 +82,7 @@ public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase { x => x.DataId, embeddingProvider.Id }, { x => x.DataName, embeddingProvider.Name }, { x => x.DataLLMProvider, embeddingProvider.UsedLLMProvider }, + { x => x.DataCustomIconDataUrl, embeddingProvider.CustomIconDataUrl }, { x => x.DataModel, embeddingProvider.Model }, { x => x.DataHostname, embeddingProvider.Hostname }, { x => x.IsSelfHosted, embeddingProvider.IsSelfHosted }, diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor index c7dea430..59504730 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor @@ -22,11 +22,16 @@ - @context.Key + @if (context.Key is LLMProviders llmProvider) + { + + } - @context.InstanceName + + + @this.GetLLMProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs index eef516da..d7096a8f 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelProviders.razor.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using AIStudio.Dialogs; -using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -20,7 +19,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase { Expandable = true, IsInitiallyExpanded = false, - Selector = provider => provider.UsedLLMProvider.ToName(), + Selector = provider => provider.UsedLLMProvider, }; [Parameter] @@ -77,6 +76,7 @@ public partial class SettingsPanelProviders : SettingsPanelProviderBase { x => x.DataId, provider.Id }, { x => x.DataInstanceName, provider.InstanceName }, { x => x.DataLLMProvider, provider.UsedLLMProvider }, + { x => x.DataCustomIconDataUrl, provider.CustomIconDataUrl }, { x => x.DataModel, provider.Model }, { x => x.DataHostname, provider.Hostname }, { x => x.IsSelfHosted, provider.IsSelfHosted }, diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor index 40ec1a82..b0d51e9a 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor @@ -26,11 +26,16 @@ - @context.Key + @if (context.Key is LLMProviders llmProvider) + { + + } - @context.Name + + + @this.GetTranscriptionProviderModelName(context) diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs index 4d8daa15..252a95a4 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor.cs @@ -1,5 +1,4 @@ using AIStudio.Dialogs; -using AIStudio.Provider; using AIStudio.Settings; using Microsoft.AspNetCore.Components; @@ -18,7 +17,7 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase { Expandable = true, IsInitiallyExpanded = false, - Selector = provider => provider.UsedLLMProvider.ToName(), + Selector = provider => provider.UsedLLMProvider, }; [Parameter] @@ -81,6 +80,7 @@ public partial class SettingsPanelTranscription : SettingsPanelProviderBase { x => x.DataId, transcriptionProvider.Id }, { x => x.DataName, transcriptionProvider.Name }, { x => x.DataLLMProvider, transcriptionProvider.UsedLLMProvider }, + { x => x.DataCustomIconDataUrl, transcriptionProvider.CustomIconDataUrl }, { x => x.DataModel, transcriptionProvider.Model }, { x => x.DataHostname, transcriptionProvider.Hostname }, { x => x.IsSelfHosted, transcriptionProvider.IsSelfHosted }, diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor index 7cdae497..1c7c39a3 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor @@ -50,7 +50,14 @@ @foreach (var embedding in this.AvailableEmbeddings) { - @embedding.Name + @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) + { + + } + else + { + @embedding.Name + } } @@ -109,4 +116,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs index 42463e38..d103336e 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalDirectoryDialog.razor.cs @@ -95,6 +95,12 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase } #endregion + + private EmbeddingProvider? GetEmbeddingProvider(string providerId) + { + var provider = this.SettingsManager.GetEmbeddingProviderById(providerId); + return provider == EmbeddingProvider.NONE ? null : provider; + } private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false); @@ -124,4 +130,4 @@ public partial class DataSourceLocalDirectoryDialog : MSGComponentBase } private void Cancel() => this.MudDialog.Cancel(); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor index d360b0de..b9cba6b5 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor @@ -50,7 +50,14 @@ @foreach (var embedding in this.AvailableEmbeddings) { - @embedding.Name + @if (this.GetEmbeddingProvider(embedding.Value) is { } provider) + { + + } + else + { + @embedding.Name + } } @@ -109,4 +116,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs index 13b8df1e..df794cca 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceLocalFileDialog.razor.cs @@ -95,6 +95,12 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase } #endregion + + private EmbeddingProvider? GetEmbeddingProvider(string providerId) + { + var provider = this.SettingsManager.GetEmbeddingProviderById(providerId); + return provider == EmbeddingProvider.NONE ? null : provider; + } private bool SelectedCloudEmbedding => !(this.SettingsManager.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id == this.dataEmbeddingId)?.IsTrustedForDataSourceSecurityChecks(this.SettingsManager) ?? false); @@ -124,4 +130,4 @@ public partial class DataSourceLocalFileDialog : MSGComponentBase } private void Cancel() => this.MudDialog.Cancel(); -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor index 1abb680e..5bcead75 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor @@ -13,13 +13,13 @@ @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE) { - @provider.ToName() + } } @@ -152,4 +152,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs index f2fb87f6..4bb3c189 100644 --- a/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/EmbeddingProviderDialog.razor.cs @@ -56,6 +56,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE; + + /// + /// The validated custom icon supplied by a configuration plugin. + /// + [Parameter] + public string DataCustomIconDataUrl { get; set; } = string.Empty; /// /// The embedding model to use. @@ -144,6 +150,7 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId Host = this.DataHost, IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, + CustomIconDataUrl = this.DataCustomIconDataUrl, }; } @@ -343,4 +350,4 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId }; private bool IsNoneProvider => this.DataLLMProvider is LLMProviders.NONE; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor index b896837e..eb97b251 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor @@ -18,15 +18,12 @@ ValueChanged="@this.OnProviderChanged" Label="@T("Provider")" Class="mb-3" - OpenIcon="@Icons.Material.Filled.AccountBalance" - AdornmentColor="Color.Info" - Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider"> @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { - @provider.ToName() + } diff --git a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs index ef14b7bb..ae8362c3 100644 --- a/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/ProviderDialog.razor.cs @@ -78,6 +78,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE; + + /// + /// The validated custom icon supplied by a configuration plugin. + /// + [Parameter] + public string DataCustomIconDataUrl { get; set; } = string.Empty; /// /// The LLM model to use, e.g., GPT-4o. @@ -184,6 +190,7 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId HFInferenceProvider = this.HFInferenceProviderId, AdditionalJsonApiParameters = this.AdditionalJsonApiParameters, CapabilityOverrides = this.capabilityOverrides.HasOverrides ? this.capabilityOverrides : null, + CustomIconDataUrl = this.DataCustomIconDataUrl, }; } diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor index dd7fb33a..e92889a4 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor @@ -13,13 +13,13 @@ @* ReSharper disable once CSharpWarnings::CS8974 *@ - + @foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders))) { if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE) { - @provider.ToName() + } } @@ -163,4 +163,4 @@ } - \ No newline at end of file + diff --git a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs index 9132c9e6..1e1081f3 100644 --- a/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/TranscriptionProviderDialog.razor.cs @@ -56,6 +56,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId /// [Parameter] public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE; + + /// + /// The validated custom icon supplied by a configuration plugin. + /// + [Parameter] + public string DataCustomIconDataUrl { get; set; } = string.Empty; /// /// The transcription model to use. @@ -159,6 +165,7 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId Host = this.DataHost, IsEnterpriseConfiguration = this.IsEnterpriseConfiguration, EnterpriseConfigurationPluginId = Guid.Empty, + CustomIconDataUrl = this.DataCustomIconDataUrl, }; } @@ -358,4 +365,4 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId }; private bool IsNoneProvider => this.DataLLMProvider is LLMProviders.NONE; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 272265c8..34714d92 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -393,8 +393,20 @@ + + + + @T("AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies.") + + + @T("Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider.") + + + @T("Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides.") + + diff --git a/app/MindWork AI Studio/Pages/Plugins.razor b/app/MindWork AI Studio/Pages/Plugins.razor index 4ceaa044..e99f7c4e 100644 --- a/app/MindWork AI Studio/Pages/Plugins.razor +++ b/app/MindWork AI Studio/Pages/Plugins.razor @@ -63,7 +63,10 @@
- @((MarkupString)context.IconSVG) + @if (!string.IsNullOrEmpty(context.IconDataUrl)) + { + + }
diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 3f06e79d..04650377 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -94,6 +94,19 @@ CONFIG["LLM_PROVIDERS"] = {} -- -- Please do not add the enclosing curly braces {} here. Also, no trailing comma is allowed. -- ["AdditionalJsonApiParameters"] = "", -- +-- -- Optional: replace the built-in provider logo with a project-specific icon. +-- -- The path is relative to this plugin.lua and must point to an SVG file inside +-- -- this plugin directory, for example: assets/project-icon.svg. Absolute paths, +-- -- parent-directory segments (..), links leaving the plugin directory, files +-- -- larger than 32 KiB, and files which are not well-formed SVG are rejected. +-- -- An invalid or missing icon logs a warning while the provider still loads +-- -- with its built-in logo. AI Studio shows every icon in an isolated image +-- -- element, so scripts or external references inside an SVG never run; for the +-- -- same reason, the icon cannot inherit colors from the app and has to bring +-- -- its own. Provide a single icon with enough contrast on both light and dark +-- -- surfaces. +-- -- ["IconPath"] = "assets/project-icon.svg", +-- -- -- Optional: expert capability overrides. -- -- Allowed keys are exactly: -- -- AUDIO_INPUT, MULTIPLE_IMAGE_INPUT, SPEECH_INPUT, VIDEO_INPUT, @@ -146,6 +159,10 @@ CONFIG["TRANSCRIPTION_PROVIDERS"] = {} -- ["Host"] = "WHISPER_CPP", -- ["Hostname"] = "", -- +-- -- Optional: project-specific SVG icon. The same path, size, rendering, and +-- -- fallback rules described for IconPath under LLM_PROVIDERS apply here. +-- -- ["IconPath"] = "assets/project-icon.svg", +-- -- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details) -- -- ["APIKey"] = "ENC:v1:", -- @@ -173,6 +190,10 @@ CONFIG["EMBEDDING_PROVIDERS"] = {} -- ["Host"] = "OLLAMA", -- ["Hostname"] = "", -- +-- -- Optional: project-specific SVG icon. The same path, size, rendering, and +-- -- fallback rules described for IconPath under LLM_PROVIDERS apply here. +-- -- ["IconPath"] = "assets/project-icon.svg", +-- -- -- Optional: Encrypted API key (see LLM_PROVIDERS example for details) -- -- ["APIKey"] = "ENC:v1:", -- diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 2badc9c3..9e7cf65d 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -8322,6 +8322,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1402243995"] = "Updates werden v -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "Diese Bibliothek wird verwendet, um die MudBlazor-Bibliothek zu erweitern. Sie stellt zusätzliche Komponenten bereit, die nicht Teil der MudBlazor-Bibliothek sind." +-- Trademarks & Brand Assets +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421823619"] = "Marken und Markenressourcen" + -- Copies the allowed host pattern to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Kopiert das zulässige Hostmuster in die Zwischenablage" @@ -8331,6 +8334,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Warten auf das K -- Encryption secret: is not configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Geheimnis für die Verschlüsselung: ist nicht konfiguriert" +-- Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T158845920"] = "Organisationen können diese Logos über ein Konfigurations-Plugin durch eigene Symbole ersetzen. In diesem Fall ist Ihre Organisation dafür verantwortlich, die Rechte an den bereitgestellten Symbolen zu besitzen." + -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio wird mit Unternehmenskonfigurationen und Konfigurationsservern betrieben. Die Konfigurations-Plugins sind aktiv." @@ -8346,6 +8352,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio erstel -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin-Verzeichnis:" +-- Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1699089284"] = "Mehrere Anbieterlogos in AI Studio verwenden die vom Projekt Simple Icons veröffentlichten Icon-Pfade und Markenfarben, die unter CC0 gemeinfrei bereitgestellt werden. Die Marken selbst sind nicht Teil dieser Freigabe und bleiben Eigentum ihrer jeweiligen Inhaber." + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Zustimmung:" @@ -8388,6 +8397,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgende -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Kopiert die Server-URL in die Zwischenablage" +-- AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio zeigt neben dem jeweiligen Eintrag das Logo eines KI-Anbieters, damit Sie auf einen Blick sehen können, mit welchem Dienst ein Anbieter verbunden ist. Alle Produktnamen, Logos und Marken sind Eigentum ihrer jeweiligen Inhaber. Ihre Verwendung dient hier ausschließlich der Kennzeichnung kompatibler Dienste und bedeutet keine Empfehlung, Unterstützung oder Geschäftsbeziehung zwischen MindWork AI Studio und diesen Unternehmen." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "Das Projekt windows-rs ermöglicht den Zugriff auf Windows-APIs aus Rust. Wir verwenden mehrere Bibliotheken aus diesem Projekt: windows-registry wird verwendet, um die gewünschte Konfiguration in Windows-Unternehmensumgebungen auszulesen. Die Bibliotheken windows und windows-collections werden verwendet, um den nativen Windows-Dialog zum Teilen zu öffnen, zum Beispiel wenn Sie ein Plugin mit anderen teilen." @@ -8652,6 +8664,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssyst -- Allowed host: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Zulässiger Host:" +-- Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3775183188"] = "Einige dieser Logos stammen aus dem Projekt Simple Icons, das sie unter CC0 veröffentlicht. Die übrigen wurden den offiziellen Markenressourcen des jeweiligen Anbieters entnommen. Jedes Logo wird mit AI Studio ausgeliefert und von Ihrem Gerät geladen. Beim Anzeigen wird daher keine Anfrage an den Anbieter gesendet." + -- Configuration source: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Quelle der Konfiguration:" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index b95a069e..228a1c3f 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -8322,6 +8322,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1402243995"] = "Updates are mana -- This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421513382"] = "This library is used to extend the MudBlazor library. It provides additional components that are not part of the MudBlazor library." +-- Trademarks & Brand Assets +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1421823619"] = "Trademarks & Brand Assets" + -- Copies the allowed host pattern to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1513592659"] = "Copies the allowed host pattern to the clipboard" @@ -8331,6 +8334,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1533382393"] = "Waiting for the -- Encryption secret: is not configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1560776885"] = "Encryption secret: is not configured" +-- Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T158845920"] = "Organizations can replace these logos with their own icons through a configuration plugin. When your organization does so, it is responsible for holding the rights to the icons it provides." + -- AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1596483935"] = "AI Studio runs with an enterprise configuration and configuration servers. The configuration plugins are active." @@ -8346,6 +8352,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1630237140"] = "AI Studio create -- Plugin directory: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1698127325"] = "Plugin directory:" +-- Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1699089284"] = "Several of the provider logos in AI Studio use the icon paths and brand colors published by the Simple Icons project, which releases them into the public domain under CC0. The trademarks themselves are not part of that release and remain the property of their respective owners." + -- Consent: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T171952677"] = "Consent:" @@ -8388,6 +8397,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the follo -- Copies the server URL to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2037899437"] = "Copies the server URL to the clipboard" +-- AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2124655767"] = "AI Studio shows the logo of an AI provider next to its entry, so you can see at a glance which service a provider connects to. All product names, logos, and trademarks are the property of their respective owners. Their use here identifies compatible services and implies no endorsement, sponsorship, or business relationship between MindWork AI Studio and these companies." + -- The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2146481269"] = "The windows-rs project provides access to Windows APIs from Rust. We use several libraries from this project: windows-registry is used to read the desired configuration in Windows enterprise environments. The windows and windows-collections libraries are used to open the native Windows share dialog, e.g., when you share a plugin with others." @@ -8652,6 +8664,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provide -- Allowed host: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3774270763"] = "Allowed host:" +-- Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3775183188"] = "Some of these logos come from the Simple Icons project, which publishes them under CC0. The remaining ones were taken from the official brand resources of the respective provider. Every logo ships with AI Studio and is loaded from your device, so showing it never sends a request to the provider." + -- Configuration source: UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3801531724"] = "Configuration source:" diff --git a/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs new file mode 100644 index 00000000..d47ff27a --- /dev/null +++ b/app/MindWork AI Studio/Provider/LLMProvidersIconExtensions.cs @@ -0,0 +1,42 @@ +namespace AIStudio.Provider; + +public static class LLMProvidersIconExtensions +{ + private const string ICON_ROOT = "/images/provider-icons"; + private const string SVG_DATA_URL_PREFIX = "data:image/svg+xml;base64,"; + + public static string GetIconUrl(this AIStudio.Settings.Provider provider, bool isDarkMode) + => provider.UsedLLMProvider.GetIconUrl(isDarkMode, provider.CustomIconDataUrl); + + public static string GetIconUrl(this LLMProviders provider, bool isDarkMode, string? customIconDataUrl) + { + if (customIconDataUrl?.StartsWith(SVG_DATA_URL_PREFIX, StringComparison.Ordinal) == true) + return customIconDataUrl; + + return provider.GetIconUrl(isDarkMode); + } + + public static string GetIconUrl(this LLMProviders provider, bool isDarkMode) => provider switch + { + LLMProviders.NONE => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", + LLMProviders.OPEN_AI => $"{ICON_ROOT}/openai{DarkVariant(isDarkMode)}.svg", + LLMProviders.ANTHROPIC => $"{ICON_ROOT}/anthropic{DarkVariant(isDarkMode)}.svg", + LLMProviders.MISTRAL => $"{ICON_ROOT}/mistral.svg", + LLMProviders.GOOGLE => $"{ICON_ROOT}/google.svg", + LLMProviders.X => $"{ICON_ROOT}/x{DarkVariant(isDarkMode)}.svg", + LLMProviders.DEEP_SEEK => $"{ICON_ROOT}/deepseek.svg", + LLMProviders.ALIBABA_CLOUD => $"{ICON_ROOT}/alibaba-cloud.svg", + LLMProviders.PERPLEXITY => $"{ICON_ROOT}/perplexity.svg", + LLMProviders.OPEN_ROUTER => $"{ICON_ROOT}/openrouter.svg", + LLMProviders.HETZNER => $"{ICON_ROOT}/hetzner.svg", + LLMProviders.GROQ => $"{ICON_ROOT}/groq.svg", + LLMProviders.FIREWORKS => $"{ICON_ROOT}/fireworks.svg", + LLMProviders.HUGGINGFACE => $"{ICON_ROOT}/hugging-face.svg", + LLMProviders.SELF_HOSTED => $"{ICON_ROOT}/self-hosted{DarkVariant(isDarkMode)}.svg", + LLMProviders.HELMHOLTZ => $"{ICON_ROOT}/helmholtz.svg", + LLMProviders.GWDG => $"{ICON_ROOT}/gwdg.svg", + _ => $"{ICON_ROOT}/provider{DarkVariant(isDarkMode)}.svg", + }; + + private static string DarkVariant(bool isDarkMode) => isDarkMode ? "-dark" : string.Empty; +} diff --git a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs index b8306d58..61324df3 100644 --- a/app/MindWork AI Studio/Settings/EmbeddingProvider.cs +++ b/app/MindWork AI Studio/Settings/EmbeddingProvider.cs @@ -21,7 +21,8 @@ public sealed record EmbeddingProvider( Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", Host Host = Host.NONE, - bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey + bool AllowUserProvidedAPIKey = false, + string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -53,7 +54,7 @@ public sealed record EmbeddingProvider( #endregion - public static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider) + public static bool TryParseEmbeddingProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider) { provider = NONE; if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) @@ -102,6 +103,15 @@ public sealed record EmbeddingProvider( if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + var customIconDataUrl = string.Empty; + if (table.TryGetValue("IconPath", out var iconPathValue)) + { + if (!iconPathValue.TryRead(out var iconPath)) + LOGGER.LogWarning($"The configured embedding provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})"); + else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue)) + LOGGER.LogWarning($"The configured embedding provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})"); + } + provider = new EmbeddingProvider { Num = 0, // will be set later by the PluginConfigurationObject @@ -115,6 +125,7 @@ public sealed record EmbeddingProvider( Hostname = hostname, Host = host, AllowUserProvidedAPIKey = allowUserProvidedApiKey, + CustomIconDataUrl = customIconDataUrl, }; // Handle an encrypted API key if present. When the user manages their own key for this diff --git a/app/MindWork AI Studio/Settings/PluginIconFile.cs b/app/MindWork AI Studio/Settings/PluginIconFile.cs new file mode 100644 index 00000000..224b1a17 --- /dev/null +++ b/app/MindWork AI Studio/Settings/PluginIconFile.cs @@ -0,0 +1,125 @@ +namespace AIStudio.Settings; + +/// +/// Loads an icon file a configuration plugin points to, such as a custom provider logo. +/// +/// +/// The checks here are about the path, not about the markup: they keep a plugin from turning an +/// arbitrary file somewhere on the system into a data URL. Whether the file is a usable SVG, and +/// how it becomes a data URL, is up to SvgIcon. +/// +internal static class PluginIconFile +{ + public static bool TryLoadDataUrl(string iconPath, string pluginPath, out string dataUrl, out string issue) + { + dataUrl = string.Empty; + issue = string.Empty; + + if (string.IsNullOrWhiteSpace(iconPath)) + { + issue = "The icon path is empty."; + return false; + } + + if (Path.IsPathFullyQualified(iconPath)) + { + issue = "The icon path must be relative to the configuration plugin directory."; + return false; + } + + if (string.IsNullOrWhiteSpace(pluginPath)) + { + issue = "The icon path cannot be resolved because the configuration plugin directory is unknown."; + return false; + } + + var relativePath = iconPath + .Replace('/', Path.DirectorySeparatorChar) + .Replace('\\', Path.DirectorySeparatorChar); + + if (relativePath.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == "..")) + { + issue = "The icon path must not contain '..' path segments."; + return false; + } + + string pluginRoot; + string resolvedPath; + try + { + pluginRoot = Path.GetFullPath(pluginPath); + resolvedPath = Path.GetFullPath(Path.Combine(pluginRoot, relativePath)); + } + catch (Exception e) + { + issue = $"The icon path is invalid: {e.Message}"; + return false; + } + + if (!IsInsideDirectory(pluginRoot, resolvedPath)) + { + issue = "The icon path points outside of the configuration plugin directory."; + return false; + } + + if (!string.Equals(Path.GetExtension(resolvedPath), ".svg", StringComparison.OrdinalIgnoreCase)) + { + issue = "The icon file must use the .svg extension."; + return false; + } + + if (!File.Exists(resolvedPath)) + { + issue = "The icon file does not exist."; + return false; + } + + try + { + // Check the size before reading, so an oversized file never reaches memory: + var fileInfo = new FileInfo(resolvedPath); + if (fileInfo.Length is <= 0 or > SvgIcon.MAX_ICON_SIZE_BYTES) + { + issue = $"The icon file must be between 1 byte and {SvgIcon.MAX_ICON_SIZE_BYTES / 1024} KiB."; + return false; + } + + if (!LinksStayInsideDirectory(pluginRoot, resolvedPath)) + { + issue = "The icon path contains a link that points outside of the configuration plugin directory."; + return false; + } + + return SvgIcon.TryCreateDataUrl(File.ReadAllBytes(resolvedPath), out dataUrl, out issue); + } + catch (Exception e) + { + issue = $"The icon file could not be read: {e.Message}"; + return false; + } + } + + private static bool LinksStayInsideDirectory(string rootDirectory, string filePath) + { + var relativePath = Path.GetRelativePath(rootDirectory, filePath); + var currentPath = Path.GetFullPath(rootDirectory); + foreach (var segment in relativePath.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar], StringSplitOptions.RemoveEmptyEntries)) + { + currentPath = Path.Combine(currentPath, segment); + FileSystemInfo pathInfo = Directory.Exists(currentPath) ? new DirectoryInfo(currentPath) : new FileInfo(currentPath); + var finalTarget = pathInfo.ResolveLinkTarget(true); + if (finalTarget is not null && !IsInsideDirectory(rootDirectory, finalTarget.FullName)) + return false; + } + + return true; + } + + private static bool IsInsideDirectory(string rootDirectory, string path) + { + var root = Path.GetFullPath(rootDirectory).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + var target = Path.GetFullPath(path); + var comparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; + return target.StartsWith(root, comparison); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/Provider.cs b/app/MindWork AI Studio/Settings/Provider.cs index fdf63a34..406af35a 100644 --- a/app/MindWork AI Studio/Settings/Provider.cs +++ b/app/MindWork AI Studio/Settings/Provider.cs @@ -22,6 +22,7 @@ namespace AIStudio.Settings; /// The hostname of the provider. Useful for self-hosted providers. /// The LLM model to use for chat. /// When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider. +/// The validated custom SVG icon supplied by a configuration plugin. public sealed record Provider( uint Num, string Id, @@ -36,7 +37,8 @@ public sealed record Provider( HFInferenceProvider HFInferenceProvider = HFInferenceProvider.NONE, string AdditionalJsonApiParameters = "", ProviderCapabilityOverrides? CapabilityOverrides = null, - bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey + bool AllowUserProvidedAPIKey = false, + string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -93,7 +95,7 @@ public sealed record Provider( #endregion - public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider) + public static bool TryParseProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider) { provider = NONE; if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) @@ -161,6 +163,15 @@ public sealed record Provider( if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + var customIconDataUrl = string.Empty; + if (table.TryGetValue("IconPath", out var iconPathValue)) + { + if (!iconPathValue.TryRead(out var iconPath)) + LOGGER.LogWarning($"The configured provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})"); + else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue)) + LOGGER.LogWarning($"The configured provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})"); + } + provider = new Provider { Num = 0, // will be set later by the PluginConfigurationObject @@ -177,6 +188,7 @@ public sealed record Provider( AdditionalJsonApiParameters = additionalJsonApiParameters, CapabilityOverrides = capabilityOverrides, AllowUserProvidedAPIKey = allowUserProvidedApiKey, + CustomIconDataUrl = customIconDataUrl, }; // Handle an encrypted API key if present. When the user manages their own key for this diff --git a/app/MindWork AI Studio/Settings/SettingsManager.cs b/app/MindWork AI Studio/Settings/SettingsManager.cs index 1242c78e..527f3e2c 100644 --- a/app/MindWork AI Studio/Settings/SettingsManager.cs +++ b/app/MindWork AI Studio/Settings/SettingsManager.cs @@ -660,6 +660,29 @@ public sealed class SettingsManager .ThenBy(x => x.Num) .ToList(); + /// + /// Returns the embedding provider with the given id, without applying any confidence filtering. + /// + /// + /// This method resolves a stored embedding provider reference by its id. It applies neither the + /// global minimum confidence level nor any component-specific minimum, so it returns the + /// requested embedding provider even when the user enforces a higher global minimum. Callers + /// that intend to send data to the returned embedding provider must check it themselves, for + /// example through IsTrustedForDataSourceSecurityChecks. + /// + /// The id of the embedding provider to look up. + /// The embedding provider, or EmbeddingProvider.NONE when no embedding provider with that id exists. + public EmbeddingProvider GetEmbeddingProviderById(string? embeddingProviderId) + { + if (string.IsNullOrWhiteSpace(embeddingProviderId)) + return EmbeddingProvider.NONE; + + if (string.Equals(embeddingProviderId, EmbeddingProvider.NONE.Id, StringComparison.OrdinalIgnoreCase)) + return EmbeddingProvider.NONE; + + return this.ConfigurationData.EmbeddingProviders.FirstOrDefault(x => x.Id.Equals(embeddingProviderId, StringComparison.OrdinalIgnoreCase)) ?? EmbeddingProvider.NONE; + } + /// /// Returns all configured transcription providers. /// @@ -675,6 +698,29 @@ public sealed class SettingsManager .ThenBy(x => x.Num) .ToList(); + /// + /// Returns the transcription provider with the given id, without applying any confidence filtering. + /// + /// + /// This method resolves a stored transcription provider reference by its id. It applies neither + /// the global minimum confidence level nor any component-specific minimum, so it returns the + /// requested transcription provider even when the user enforces a higher global minimum. Callers + /// that intend to send audio to the returned transcription provider must check its confidence + /// level themselves, the way GetFilteredTranscriptionProviders does for the app settings. + /// + /// The id of the transcription provider to look up. + /// The transcription provider, or TranscriptionProvider.NONE when no transcription provider with that id exists. + public TranscriptionProvider GetTranscriptionProviderById(string? transcriptionProviderId) + { + if (string.IsNullOrWhiteSpace(transcriptionProviderId)) + return TranscriptionProvider.NONE; + + if (string.Equals(transcriptionProviderId, TranscriptionProvider.NONE.Id, StringComparison.OrdinalIgnoreCase)) + return TranscriptionProvider.NONE; + + return this.ConfigurationData.TranscriptionProviders.FirstOrDefault(x => x.Id.Equals(transcriptionProviderId, StringComparison.OrdinalIgnoreCase)) ?? TranscriptionProvider.NONE; + } + public Profile GetPreselectedProfile(Tools.Components component) { var preselection = component.GetProfilePreselection(this); diff --git a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs index 5afae679..7ecc7938 100644 --- a/app/MindWork AI Studio/Settings/TranscriptionProvider.cs +++ b/app/MindWork AI Studio/Settings/TranscriptionProvider.cs @@ -21,7 +21,8 @@ public sealed record TranscriptionProvider( Guid EnterpriseConfigurationPluginId = default, string Hostname = "http://localhost:1234", Host Host = Host.NONE, - bool AllowUserProvidedAPIKey = false) : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey + bool AllowUserProvidedAPIKey = false, + string CustomIconDataUrl = "") : ConfigurationBaseObject, ISecretId, IUserProvidedAPIKey { private static readonly ILogger LOGGER = Program.LOGGER_FACTORY.CreateLogger(); @@ -53,7 +54,7 @@ public sealed record TranscriptionProvider( #endregion - public static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, Guid configPluginId, out ConfigurationBaseObject provider) + public static bool TryParseTranscriptionProviderTable(int idx, LuaTable table, Guid configPluginId, string pluginPath, out ConfigurationBaseObject provider) { provider = NONE; if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead(out var idText) || !Guid.TryParse(idText, out var id)) @@ -102,6 +103,15 @@ public sealed record TranscriptionProvider( if (table.TryGetValue("AllowUserProvidedAPIKey", out var allowUserProvidedApiKeyValue) && allowUserProvidedApiKeyValue.TryRead(out var allowUserProvidedApiKeyBool)) allowUserProvidedApiKey = allowUserProvidedApiKeyBool; + var customIconDataUrl = string.Empty; + if (table.TryGetValue("IconPath", out var iconPathValue)) + { + if (!iconPathValue.TryRead(out var iconPath)) + LOGGER.LogWarning($"The configured transcription provider {idx} does not contain a valid icon path. Falling back to the built-in provider icon. (Plugin ID: {configPluginId})"); + else if (!PluginIconFile.TryLoadDataUrl(iconPath, pluginPath, out customIconDataUrl, out var iconIssue)) + LOGGER.LogWarning($"The configured transcription provider {idx} contains an invalid icon path. Falling back to the built-in provider icon. Issue: {iconIssue} (Plugin ID: {configPluginId})"); + } + provider = new TranscriptionProvider { Num = 0, // will be set later by the PluginConfigurationObject @@ -115,6 +125,7 @@ public sealed record TranscriptionProvider( Hostname = hostname, Host = host, AllowUserProvidedAPIKey = allowUserProvidedApiKey, + CustomIconDataUrl = customIconDataUrl, }; // Handle an encrypted API key if present. When the user manages their own key for this diff --git a/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs index 95d26b34..4115cb67 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/IPluginMetadata.cs @@ -3,9 +3,14 @@ namespace AIStudio.Tools.PluginSystem; public interface IPluginMetadata { /// - /// The icon of this plugin. + /// The icon of this plugin, as a data URL ready for the src attribute of an image element. /// - public string IconSVG { get; } + /// + /// Deliberately a data URL and not the raw markup: the icon comes from the plugin, so it must + /// never be rendered inline into the DOM. Inside an image element the browser treats it as a + /// standalone document which runs no script and loads nothing from the network. + /// + public string IconDataUrl { get; } /// /// The type of this plugin. diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs index 60f14acb..5c7fbdc9 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.Icon.cs @@ -7,39 +7,62 @@ public abstract partial class PluginBase """; + private static readonly string DEFAULT_ICON_DATA_URL = CreateDefaultIconDataUrl(); + #region Initialization-related methods /// /// Tries to initialize the icon of the plugin. /// /// - /// When no icon is specified, the default icon will be used. + /// + /// When no icon is specified, or when the specified icon is unusable, the default icon will be + /// used. A plugin never fails to load over its icon. + /// + /// + /// The icon is handed out as a data URL, not as markup: plugins are shown through an image + /// element so the browser treats their icon as a standalone, script-less document. Rendering + /// plugin-supplied markup inline would hand every plugin author a way to run code in the app. + /// /// /// The error message, when the icon could not be read. - /// The read icon as SVG. + /// The read icon as a data URL. /// True, when the icon could be read successfully. // ReSharper disable once OutParameterValueIsAlwaysDiscarded.Local // ReSharper disable once UnusedMethodReturnValue.Local - private bool TryInitIconSVG(out string message, out string iconSVG) + private bool TryInitIconDataUrl(out string message, out string iconDataUrl) { - if (!this.State.Environment["ICON_SVG"].TryRead(out iconSVG)) + if (!this.State.Environment["ICON_SVG"].TryRead(out var iconSVG)) { - iconSVG = DEFAULT_ICON_SVG; + iconDataUrl = DEFAULT_ICON_DATA_URL; message = "The field ICON_SVG does not exist or is not a valid string."; return true; } if (string.IsNullOrWhiteSpace(iconSVG)) { - iconSVG = DEFAULT_ICON_SVG; + iconDataUrl = DEFAULT_ICON_DATA_URL; message = "The field ICON_SVG is empty. The icon must be a non-empty string."; return true; } + if (!SvgIcon.TryCreateDataUrl(iconSVG, out iconDataUrl, out var issue)) + { + iconDataUrl = DEFAULT_ICON_DATA_URL; + message = $"The field ICON_SVG is not a usable icon: {issue}"; + return true; + } + message = string.Empty; return true; } + private static string CreateDefaultIconDataUrl() + { + SvgIcon.TryCreateDataUrl(DEFAULT_ICON_SVG, out var dataUrl, out _); + return dataUrl; + } + #endregion } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs index e7dc92c4..c35657e5 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginBase.cs @@ -16,8 +16,8 @@ public abstract partial class PluginBase : IPluginMetadata, IDisposable protected readonly List PluginIssues = []; /// - public string IconSVG { get; } - + public string IconDataUrl { get; } + /// public PluginType Type { get; } @@ -88,14 +88,14 @@ public abstract partial class PluginBase : IPluginMetadata, IDisposable if (this is NoPlugin or NoPluginLanguage) { this.IsInternal = isInternal; - this.IconSVG = string.Empty; + this.IconDataUrl = string.Empty; this.baseIssues = issues; return; } // Notice: when no icon is specified, the default icon will be used. - this.TryInitIconSVG(out _, out var iconSVG); - this.IconSVG = iconSVG; + this.TryInitIconDataUrl(out _, out var iconDataUrl); + this.IconDataUrl = iconDataUrl; if(this.TryInitId(out var issue, out var id)) { diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index d2cb82c9..b6115265 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -297,13 +297,13 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT this.TryProcessEnterpriseApprovedAssistantPlugins(settingsTable, dryRun); // Handle configured LLM providers: - PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun); + PluginConfigurationObject.TryParse(PluginConfigurationObjectType.LLM_PROVIDER, x => x.Providers, x => x.NextProviderNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath); // Handle configured transcription providers: - PluginConfigurationObject.TryParse(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, x => x.NextTranscriptionNum, mainTable, this.Id, ref this.configObjects, dryRun); + PluginConfigurationObject.TryParse(PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER, x => x.TranscriptionProviders, x => x.NextTranscriptionNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath); // Handle configured embedding providers: - PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun); + PluginConfigurationObject.TryParse(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, x => x.NextEmbeddingNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath); // Handle configured chat templates: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun, this.PluginPath); @@ -598,4 +598,4 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT LOG.LogWarning("The table 'INTRODUCTIONS' entry at index {Index} does not contain a valid introduction (config plugin id: {ConfigPluginId}).", i, this.Id); } } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index 07b4e3e8..3f6c1c89 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -142,11 +142,11 @@ public sealed record PluginConfigurationObject var (wasParsingSuccessful, configObject) = configObjectType switch { - PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject), + PluginConfigurationObjectType.LLM_PROVIDER => (Settings.Provider.TryParseProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != Settings.Provider.NONE, configurationObject), PluginConfigurationObjectType.CHAT_TEMPLATE => (ChatTemplate.TryParseChatTemplateTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != ChatTemplate.NO_CHAT_TEMPLATE, configurationObject), PluginConfigurationObjectType.PROFILE => (Profile.TryParseProfileTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != Profile.NO_PROFILE, configurationObject), - PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject), - PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject), + PluginConfigurationObjectType.TRANSCRIPTION_PROVIDER => (TranscriptionProvider.TryParseTranscriptionProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != TranscriptionProvider.NONE, configurationObject), + PluginConfigurationObjectType.EMBEDDING_PROVIDER => (EmbeddingProvider.TryParseEmbeddingProviderTable(i, luaObjectTable, configPluginId, pluginPath, out var configurationObject) && configurationObject != EmbeddingProvider.NONE, configurationObject), PluginConfigurationObjectType.DOCUMENT_ANALYSIS_POLICY => (DataDocumentAnalysisPolicy.TryProcessConfiguration(i, luaObjectTable, configPluginId, out var configurationObject) && configurationObject is DataDocumentAnalysisPolicy, configurationObject), _ => (false, NoConfigurationObject.INSTANCE) @@ -426,4 +426,4 @@ public sealed record PluginConfigurationObject return wasConfigurationChanged; } -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs index 9c9f2299..7491e9f1 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginMetadata.cs @@ -5,7 +5,7 @@ public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isM #region Implementation of IPluginMetadata /// - public string IconSVG { get; } = plugin.IconSVG; + public string IconDataUrl { get; } = plugin.IconDataUrl; /// public PluginType Type { get; } = plugin.Type; diff --git a/app/MindWork AI Studio/Tools/SvgIcon.cs b/app/MindWork AI Studio/Tools/SvgIcon.cs new file mode 100644 index 00000000..ffaa2246 --- /dev/null +++ b/app/MindWork AI Studio/Tools/SvgIcon.cs @@ -0,0 +1,113 @@ +using System.Text; +using System.Xml; +using System.Xml.Linq; + +namespace AIStudio.Tools; + +/// +/// Turns an SVG icon which came from outside the app into a data URL we can put into an img tag. +/// +/// +/// +/// Icons from plugins are never rendered inline. They go into an img element instead, where the +/// browser treats the SVG as a standalone document: it runs no script, fires no event handlers, and +/// loads nothing from the network. That is what makes a plugin-supplied icon harmless, so this +/// class does not try to filter active content out of the markup. +/// +/// +/// What is left to do is a size limit and the question of whether the icon is an SVG at all. The +/// latter is a diagnostic rather than a defense: it turns a broken icon into a log message and a +/// fallback instead of a broken image in the UI. +/// +/// +internal static class SvgIcon +{ + /// + /// The largest icon we accept. + /// + /// + /// Provider icons are persisted into the settings file as a data URL, so an oversized icon + /// would be written and parsed again on every single settings store. A logo needs far less. + /// + public const int MAX_ICON_SIZE_BYTES = 32 * 1024; + + private const string SVG_NAMESPACE = "http://www.w3.org/2000/svg"; + private const string DATA_URL_PREFIX = "data:image/svg+xml;base64,"; + + private static readonly XNamespace SVG = SVG_NAMESPACE; + + /// + /// Validates the given SVG and converts it into a data URL. + /// + /// The SVG markup to convert. + /// The resulting data URL, or an empty string when the icon was rejected. + /// The reason why the icon was rejected, or an empty string on success. + /// True, when the icon could be converted. + public static bool TryCreateDataUrl(string svg, out string dataUrl, out string issue) => TryCreateDataUrl(Encoding.UTF8.GetBytes(svg), out dataUrl, out issue); + + /// + public static bool TryCreateDataUrl(byte[] svg, out string dataUrl, out string issue) + { + dataUrl = string.Empty; + issue = string.Empty; + + if (svg.Length is <= 0 or > MAX_ICON_SIZE_BYTES) + { + issue = $"The icon must be between 1 byte and {MAX_ICON_SIZE_BYTES / 1024} KiB."; + return false; + } + + XDocument document; + try + { + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + MaxCharactersInDocument = MAX_ICON_SIZE_BYTES, + XmlResolver = null, + }; + + using var stream = new MemoryStream(svg, writable: false); + using var xmlReader = XmlReader.Create(stream, settings); + document = XDocument.Load(xmlReader, LoadOptions.None); + } + catch (Exception e) + { + issue = $"The icon is not valid SVG XML: {e.Message}"; + return false; + } + + if (document.Root is null || !string.Equals(document.Root.Name.LocalName, "svg", StringComparison.OrdinalIgnoreCase)) + { + issue = "The icon does not have an SVG root element."; + return false; + } + + // + // An SVG which is meant to be pasted into HTML often omits the namespace, because the HTML + // parser supplies it. A standalone document inside an img tag has no such help and would + // not render at all, so we add the namespace instead of rejecting such an icon: + // + if (document.Root.Name.Namespace == XNamespace.None) + { + // Materialize before renaming: we are about to change the very tree we walk. + foreach (var element in document.Root.DescendantsAndSelf().ToList()) + if (element.Name.Namespace == XNamespace.None) + element.Name = SVG + element.Name.LocalName; + + dataUrl = ToDataUrl(Encoding.UTF8.GetBytes(document.ToString(SaveOptions.DisableFormatting))); + return true; + } + + if (document.Root.Name.Namespace != SVG) + { + issue = "The icon root element is not in the SVG namespace."; + return false; + } + + dataUrl = ToDataUrl(svg); + return true; + } + + private static string ToDataUrl(byte[] svg) => $"{DATA_URL_PREFIX}{Convert.ToBase64String(svg)}"; +} \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/app.css b/app/MindWork AI Studio/wwwroot/app.css index 3442e246..af0ea4c0 100644 --- a/app/MindWork AI Studio/wwwroot/app.css +++ b/app/MindWork AI Studio/wwwroot/app.css @@ -71,9 +71,17 @@ height: var(--mud-icon-size-large); } -.plugin-icon-container svg { +.plugin-icon-container img { width: 100%; height: 100%; + object-fit: contain; +} + +.provider-icon { + width: 1.5em; + height: 1.5em; + object-fit: contain; + flex: none; } .mud-popover-open.InnerScrollingFix { @@ -435,4 +443,4 @@ tr:has(> .provider-group-header) .mud-icon-button { .provider-group-header { font-weight: 600; -} \ No newline at end of file +} diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md index 82f6c97c..2375e90a 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.8.2.md @@ -1,5 +1,7 @@ # v26.8.2, build 255 (2026-08-xx xx:xx UTC) - Added protection against prompt injection. Documents, web pages, and retrieved content can carry instructions written for the AI rather than for you, for example, text telling it to ignore its rules or to hand over its instructions. AI Studio now always removes such passages before the content reaches a model, while the rest of your document stays intact and usable. When something was removed, AI Studio tells you and can show you which passages it took out. You can turn off the detailed dialog in the app settings. For IT departments: the new setting `DataApp.ShowPromptInjectionAlert` lets you configure the detailed dialog for your organization. Many thanks to Sabrina `Sabrina-devops` for implementing this feature and to Simon `SimonBpunkt` for his work on the detection patterns and their translations. +- Added provider logos throughout AI Studio, making models easier to recognize at a glance. Configuration plugins can now give managed LLM, transcription, and embedding providers their own project icon with the optional `IconPath` field. +- Improved the safety of plugin symbols: AI Studio now shows the symbol of a plugin in isolation, so nothing inside a symbol can reach the rest of the app. - Improved how much memory AI Studio needs. Working with large documents used to grow the app to several gigabytes, and on macOS that memory was never handed back. AI Studio now stays at a fraction of that and returns memory to your system. This matters most on devices with little memory, such as a Raspberry Pi. - Improved the preview for large documents. It now shows you the beginning of your document instead of loading all of it, so the dialog opens right away. Your complete document still goes to the AI. - Fixed AI Studio reading a document to the end even after you closed its preview. Closing the dialog now stops that work immediately. diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md b/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md new file mode 100644 index 00000000..88246d1d --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/README.md @@ -0,0 +1,15 @@ +# Provider icon assets + +All provider icons are shipped with AI Studio and loaded locally. No icon triggers an external image request. + +## Sources + +- `alibaba-cloud.svg`, `anthropic*.svg`, `deepseek.svg`, `hetzner.svg`, `mistral.svg`, `perplexity.svg`, and `x*.svg` use paths and brand colors from [Simple Icons 16.21.0](https://github.com/simple-icons/simple-icons/tree/16.21.0), licensed under [CC0-1.0](https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md). +- `hugging-face.svg` was taken from their official website (https://huggingface.co/brand) +- `openai*.svg` uses the OpenAI mark path from [Simple Icons 15.15.0](https://github.com/simple-icons/simple-icons/blob/15.15.0/icons/openai.svg) and black/white variants following the [OpenAI Design Guidelines](https://openai.com/brand/). +- `fireworks.svg` is adapted from the [Fireworks AI site icon](https://fireworks.ai/icon0.svg). +- `groq.svg` is adapted from the [Groq site icon](https://groq.com/favicon.svg). +- `provider*.svg` and `self-hosted*.svg` are neutral project-owned fallback graphics. +- `gwdg.svg`, `openrouter.svg`, `google.svg` and `helmholtz.svg` were created by taking the official logo from their respective websites as images and creating a svg from them. + +The `-dark` files are contrast variants for dark surfaces. All product names, logos, and trademarks remain the property of their respective owners. Their inclusion identifies compatible services and does not imply endorsement. Sources were retrieved on 2026-08-24. diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/alibaba-cloud.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/alibaba-cloud.svg new file mode 100644 index 00000000..15fef0dc --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/alibaba-cloud.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic-dark.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic-dark.svg new file mode 100644 index 00000000..37ea9e62 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic-dark.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic.svg new file mode 100644 index 00000000..a4c8b37e --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/anthropic.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/deepseek.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/deepseek.svg new file mode 100644 index 00000000..29906a14 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/deepseek.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/fireworks.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/fireworks.svg new file mode 100644 index 00000000..27578a9b --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/fireworks.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/google.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/google.svg new file mode 100644 index 00000000..65f01ede --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/google.svg @@ -0,0 +1,60 @@ + + + Gemini logo + A four-point curved star with a red, yellow, green, and blue gradient on a transparent background. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/groq.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/groq.svg new file mode 100644 index 00000000..fe394102 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/groq.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/gwdg.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/gwdg.svg new file mode 100644 index 00000000..46b5f814 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/gwdg.svg @@ -0,0 +1,53 @@ + + + GWDG Digitalmedien – blaues Signet + Aus der gelieferten PNG-Vorlage extrahiertes blaues Signet ohne graues Bogenelement. + + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/helmholtz.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/helmholtz.svg new file mode 100644 index 00000000..23f3d8d6 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/helmholtz.svg @@ -0,0 +1,21 @@ + + Blablador Icon + Schwarzes, stilisiertes Gesichtssymbol auf weiß gefüllter Iconfläche; der Außenbereich ist transparent. + + + + + + + + + + + + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/hetzner.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/hetzner.svg new file mode 100644 index 00000000..1f8aacde --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/hetzner.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/hugging-face.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/hugging-face.svg new file mode 100644 index 00000000..ab959d16 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/hugging-face.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/mistral.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/mistral.svg new file mode 100644 index 00000000..643a83f8 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/mistral.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/openai-dark.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/openai-dark.svg new file mode 100644 index 00000000..28c94385 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/openai-dark.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/openai.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/openai.svg new file mode 100644 index 00000000..5cf01e53 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/openai.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/openrouter.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/openrouter.svg new file mode 100644 index 00000000..ffb335e2 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/openrouter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/perplexity.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/perplexity.svg new file mode 100644 index 00000000..cd1d4dcb --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/perplexity.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/provider-dark.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/provider-dark.svg new file mode 100644 index 00000000..c06f18e3 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/provider-dark.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/provider.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/provider.svg new file mode 100644 index 00000000..21881f72 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/provider.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted-dark.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted-dark.svg new file mode 100644 index 00000000..5e889038 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted-dark.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted.svg new file mode 100644 index 00000000..9ffac3ef --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/self-hosted.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/x-dark.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/x-dark.svg new file mode 100644 index 00000000..1595661f --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/x-dark.svg @@ -0,0 +1 @@ + diff --git a/app/MindWork AI Studio/wwwroot/images/provider-icons/x.svg b/app/MindWork AI Studio/wwwroot/images/provider-icons/x.svg new file mode 100644 index 00000000..ab0473cb --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/images/provider-icons/x.svg @@ -0,0 +1 @@ + diff --git a/documentation/Enterprise IT.md b/documentation/Enterprise IT.md index 2fc35995..2867a9f1 100644 --- a/documentation/Enterprise IT.md +++ b/documentation/Enterprise IT.md @@ -391,7 +391,7 @@ The latest example of an AI Studio configuration via configuration plugin can al - [The icon](../app/MindWork%20AI%20Studio/Plugins/configuration/icon.lua) - [The configuration with explanations](../app/MindWork%20AI%20Studio/Plugins/configuration/plugin.lua) -Please note that the icon must be an SVG vector graphic. Raster graphics like PNGs, GIFs, and others aren’t supported. You can use the sample icon, which looks like a gear. +Please note that the icon must be an SVG vector graphic. Raster graphics like PNGs, GIFs, and others aren’t supported. You can use the sample icon, which looks like a gear. AI Studio shows plugin icons in an isolated image element, so set the colors inside the SVG itself: `currentColor` and CSS rules from the app do not reach the icon. Your organization is responsible for holding the rights to any icon it ships. You can also give each configured provider its own icon, see [Giving providers your own icon](#giving-providers-your-own-icon). Currently, you can configure the following things: - Any number of LLM providers (self-hosted or cloud providers with encrypted API keys) @@ -639,3 +639,41 @@ The user's key follows the same "withdrawing a configuration" philosophy as ever document: if your configuration stops offering this provider, AI Studio removes the provider from the settings but leaves the user's key in the OS keyring rather than deleting it, in case the same provider comes back later. See [Withdrawing a configuration](#withdrawing-a-configuration). + +## Giving providers your own icon + +By default, AI Studio shows the logo of the underlying AI provider next to each provider entry. When +you would rather show your own project or department logo, point the optional `IconPath` field at an +SVG file inside your configuration plugin: + +```lua +CONFIG["LLM_PROVIDERS"][#CONFIG["LLM_PROVIDERS"]+1] = { + ["Id"] = "9072b77d-ca81-40da-be6a-861da525ef7b", + ["InstanceName"] = "Corporate OpenAI GPT-4", + ["UsedLLMProvider"] = "OPEN_AI", + ["Host"] = "NONE", + ["Hostname"] = "", + ["IconPath"] = "assets/project-icon.svg", + ["Model"] = { + ["Id"] = "gpt-4", + ["DisplayName"] = "GPT-4", + } +} +``` + +The field works the same way for embedding and transcription providers. + +The path is relative to your `plugin.lua` and must stay inside the plugin directory. AI Studio +rejects absolute paths, `..` segments, links pointing out of that directory, files which do not end +in `.svg`, files larger than 32 KiB, and anything which is not well-formed SVG. A rejected or +missing icon is never fatal: AI Studio logs a warning, and the provider loads with its built-in +logo. + +Two things to keep in mind when you prepare the icon: + +- **Colors belong inside the SVG.** AI Studio shows every icon in an isolated image element, so + `currentColor` and CSS rules from the app do not reach it. Choose colors which work on both light + and dark surfaces. +- **Your organization holds the rights.** Whatever icon you ship -- for a provider through + `IconPath`, or for the configuration plugin itself through its `icon.lua` -- your organization is + responsible for holding the rights to use it.