Added provider icons (#934)
Co-authored-by: Thorsten Sommer <SommerEngineering@users.noreply.github.com>
17
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.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
<h2 style="display:inline-block">
|
||||
Trademarks
|
||||
</h2>
|
||||
</summary>
|
||||
|
||||
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.
|
||||
|
||||
</details>
|
||||
|
||||
@ -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:"
|
||||
|
||||
|
||||
@ -1,2 +1,13 @@
|
||||
@inherits MSGComponentBase
|
||||
<ConfigurationSelect IsLocked="@this.IsLocked" OptionDescription="@T("Preselected provider")" Disabled="@(() => this.Disabled())" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate"/>
|
||||
<ConfigurationSelect IsLocked="@this.IsLocked" OptionDescription="@T("Preselected provider")" Disabled="@(() => this.Disabled())" OptionHelp="@this.HelpText()" Data="@this.FilteredData()" SelectedValue="@this.SelectedValue" SelectionUpdate="@this.SelectionUpdate">
|
||||
<ItemTemplate Context="providerData">
|
||||
@if (this.GetProvider(providerData.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderSettings="@provider" Text="@providerData.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@providerData.Name
|
||||
}
|
||||
</ItemTemplate>
|
||||
</ConfigurationSelect>
|
||||
|
||||
@ -32,7 +32,7 @@ public partial class ConfigurationProviderSelection : MSGComponentBase
|
||||
|
||||
[Parameter]
|
||||
public Func<bool> IsLocked { get; set; } = () => false;
|
||||
|
||||
|
||||
private IEnumerable<ConfigurationSelectData<string>> 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<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
|
||||
@ -5,7 +5,14 @@
|
||||
@foreach (var data in this.Data)
|
||||
{
|
||||
<MudSelectItem Value="@data.Value">
|
||||
@data.Name
|
||||
@if (this.ItemTemplate is null)
|
||||
{
|
||||
@data.Name
|
||||
}
|
||||
else
|
||||
{
|
||||
@this.ItemTemplate(data)
|
||||
}
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudSelect>
|
||||
|
||||
@ -33,7 +33,13 @@ public partial class ConfigurationSelect<TConfig> : ConfigurationBaseCore
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Func<TConfig, Task> SelectionUpdateAsync { get; set; } = _ => Task.CompletedTask;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Optional template used to render an item in the list.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment<ConfigurationSelectData<TConfig>>? ItemTemplate { get; set; }
|
||||
|
||||
#region Overrides of ConfigurationBase
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -54,4 +60,4 @@ public partial class ConfigurationSelect<TConfig> : ConfigurationBaseCore
|
||||
await this.SettingsManager.StoreSettings();
|
||||
await this.InformAboutChange();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
app/MindWork AI Studio/Components/ProviderIcon.razor
Normal file
@ -0,0 +1 @@
|
||||
<img src="@this.IconUrl" alt="" aria-hidden="true" draggable="false" class="@this.CssClass" style="@this.Style" />
|
||||
59
app/MindWork AI Studio/Components/ProviderIcon.razor.cs
Normal file
@ -0,0 +1,59 @@
|
||||
using AIStudio.Provider;
|
||||
using AIStudio.Settings;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows the icon of a provider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public partial class ProviderIcon : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The configured provider whose icon should be shown. Takes precedence over ProviderType.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider? ProviderSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The LLM provider whose icon should be shown when no ProviderSettings was given.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders ProviderType { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string CustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional CSS class for the icon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Class { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional inline style for the icon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Style { get; set; } = string.Empty;
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
4
app/MindWork AI Studio/Components/ProviderLabel.razor
Normal file
@ -0,0 +1,4 @@
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Wrap="Wrap.NoWrap">
|
||||
<ProviderIcon ProviderSettings="@this.ProviderSettings" ProviderType="@this.ProviderType" CustomIconDataUrl="@this.CustomIconDataUrl" />
|
||||
<MudText Class="@this.TextClass">@this.Text</MudText>
|
||||
</MudStack>
|
||||
46
app/MindWork AI Studio/Components/ProviderLabel.razor.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using AIStudio.Provider;
|
||||
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AIStudio.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Shows a provider icon next to its name.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public partial class ProviderLabel : ComponentBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The configured provider whose icon should be shown. Takes precedence over ProviderType.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public AIStudio.Settings.Provider? ProviderSettings { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The LLM provider whose icon should be shown when no ProviderSettings was given.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders ProviderType { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string CustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The text shown next to the icon.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string Text { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Additional CSS class for the text.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string TextClass { get; set; } = string.Empty;
|
||||
}
|
||||
@ -1,11 +1,11 @@
|
||||
@using AIStudio.Settings
|
||||
@inherits MSGComponentBase
|
||||
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Apps" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
|
||||
<MudSelect T="Provider" Value="@this.ProviderSettings" ValueChanged="@this.SelectionChanged" Validation="@this.ValidateProvider" Margin="Margin.Dense" Label="@T("Provider")" Class="mb-3 rounded-lg" OuterClass="flex-grow-0" Variant="Variant.Outlined" Disabled="@this.Disabled">
|
||||
@foreach (var providerItem in this.GetAvailableProviderSelectionItems())
|
||||
{
|
||||
<MudSelectItem Value="@providerItem.Provider">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Class="w-100" Wrap="Wrap.NoWrap">
|
||||
<MudText Class="me-2">@providerItem.Provider</MudText>
|
||||
<ProviderLabel ProviderSettings="@providerItem.Provider" Text="@providerItem.Provider.ToString()" TextClass="me-2" />
|
||||
@if (providerItem.CapabilityIcons.Count > 0)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1" Wrap="Wrap.NoWrap" Class="flex-grow-0">
|
||||
|
||||
@ -105,12 +105,24 @@ public partial class ProviderSelection : MSGComponentBase
|
||||
|
||||
#region Overrides of MSGComponentBase
|
||||
|
||||
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
|
||||
protected override async Task ProcessIncomingMessage<T>(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<CapabilityIcon> CapabilityIcons);
|
||||
}
|
||||
}
|
||||
|
||||
@ -37,7 +37,18 @@
|
||||
|
||||
@if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager))
|
||||
{
|
||||
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked"/>
|
||||
<ConfigurationSelect OptionDescription="@T("Select a transcription provider")" SelectedValue="@(() => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider)" Data="@this.GetFilteredTranscriptionProviders()" SelectionUpdate="@(selectedValue => this.SettingsManager.ConfigurationData.App.UseTranscriptionProvider = selectedValue)" OptionHelp="@T("Select a transcription provider for transcribing your voice. Without a selected provider, dictation and transcription features will be disabled.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.UseTranscriptionProvider, out var meta) && meta.IsLocked">
|
||||
<ItemTemplate Context="providerData">
|
||||
@if (this.GetTranscriptionProvider(providerData.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@provider.UsedLLMProvider" CustomIconDataUrl="@provider.CustomIconDataUrl" Text="@providerData.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@providerData.Name
|
||||
}
|
||||
</ItemTemplate>
|
||||
</ConfigurationSelect>
|
||||
<ConfigurationShortcut Data="@this.VoiceRecordingShortcut" OptionDescription="@T("Voice recording shortcut")" OptionHelp="@T("The global keyboard shortcut for toggling voice recording. This shortcut works system-wide, even when the app is not focused.")" IsLocked="() => ManagedConfiguration.TryGet(x => x.App, x => x.ShortcutVoiceRecording, out var meta) && meta.IsLocked"/>
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -30,11 +30,16 @@
|
||||
</HeaderContent>
|
||||
<GroupHeaderTemplate>
|
||||
<MudTh Class="provider-group-header" colspan="4">
|
||||
@context.Key
|
||||
@if (context.Key is LLMProviders llmProvider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@llmProvider" Text="@llmProvider.ToName()" />
|
||||
}
|
||||
</MudTh>
|
||||
</GroupHeaderTemplate>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>
|
||||
<ProviderLabel ProviderType="@context.UsedLLMProvider" CustomIconDataUrl="@context.CustomIconDataUrl" Text="@context.Name" />
|
||||
</MudTd>
|
||||
<MudTd>@this.GetEmbeddingProviderModelName(context)</MudTd>
|
||||
|
||||
<MudTd>
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -22,11 +22,16 @@
|
||||
</HeaderContent>
|
||||
<GroupHeaderTemplate>
|
||||
<MudTh Class="provider-group-header" colspan="4">
|
||||
@context.Key
|
||||
@if (context.Key is LLMProviders llmProvider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@llmProvider" Text="@llmProvider.ToName()" />
|
||||
}
|
||||
</MudTh>
|
||||
</GroupHeaderTemplate>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.InstanceName</MudTd>
|
||||
<MudTd>
|
||||
<ProviderLabel ProviderSettings="@context" Text="@context.InstanceName" />
|
||||
</MudTd>
|
||||
<MudTd>@this.GetLLMProviderModelName(context)</MudTd>
|
||||
<MudTd>
|
||||
<MudStack Row="true" Class="mb-2 mt-2" Spacing="1" Wrap="Wrap.Wrap">
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -26,11 +26,16 @@
|
||||
</HeaderContent>
|
||||
<GroupHeaderTemplate>
|
||||
<MudTh Class="provider-group-header" colspan="4">
|
||||
@context.Key
|
||||
@if (context.Key is LLMProviders llmProvider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@llmProvider" Text="@llmProvider.ToName()" />
|
||||
}
|
||||
</MudTh>
|
||||
</GroupHeaderTemplate>
|
||||
<RowTemplate>
|
||||
<MudTd>@context.Name</MudTd>
|
||||
<MudTd>
|
||||
<ProviderLabel ProviderType="@context.UsedLLMProvider" CustomIconDataUrl="@context.CustomIconDataUrl" Text="@context.Name" />
|
||||
</MudTd>
|
||||
<MudTd>@this.GetTranscriptionProviderModelName(context)</MudTd>
|
||||
|
||||
<MudTd>
|
||||
|
||||
@ -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 },
|
||||
|
||||
@ -50,7 +50,14 @@
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">
|
||||
@embedding.Name
|
||||
@if (this.GetEmbeddingProvider(embedding.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@provider.UsedLLMProvider" CustomIconDataUrl="@provider.CustomIconDataUrl" Text="@embedding.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@embedding.Name
|
||||
}
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@ -109,4 +116,4 @@
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +50,14 @@
|
||||
@foreach (var embedding in this.AvailableEmbeddings)
|
||||
{
|
||||
<MudSelectItem Value="@embedding.Value">
|
||||
@embedding.Name
|
||||
@if (this.GetEmbeddingProvider(embedding.Value) is { } provider)
|
||||
{
|
||||
<ProviderLabel ProviderType="@provider.UsedLLMProvider" CustomIconDataUrl="@provider.CustomIconDataUrl" Text="@embedding.Name" />
|
||||
}
|
||||
else
|
||||
{
|
||||
@embedding.Name
|
||||
}
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
@ -109,4 +116,4 @@
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,13 +13,13 @@
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
if (provider.ProvideEmbeddingAPI() || provider is LLMProviders.NONE)
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
@ -152,4 +152,4 @@
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -56,6 +56,12 @@ public partial class EmbeddingProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)))
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@ -78,6 +78,12 @@ public partial class ProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -13,13 +13,13 @@
|
||||
<MudForm @ref="@this.form" @bind-IsValid="@this.dataIsValid" @bind-Errors="@this.dataIssues">
|
||||
<MudStack Row="@true" AlignItems="AlignItems.Center">
|
||||
@* ReSharper disable once CSharpWarnings::CS8974 *@
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" OpenIcon="@Icons.Material.Filled.AccountBalance" AdornmentColor="Color.Info" Adornment="Adornment.Start" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
<MudSelect @bind-Value="@this.DataLLMProvider" Label="@T("Provider")" Class="mb-3" Disabled="@this.IsEnterpriseConfiguration" Validation="@this.providerValidation.ValidatingProvider">
|
||||
@foreach (LLMProviders provider in Enum.GetValues(typeof(LLMProviders)))
|
||||
{
|
||||
if (provider.ProvideTranscriptionAPI() || provider is LLMProviders.NONE)
|
||||
{
|
||||
<MudSelectItem Value="@provider">
|
||||
@provider.ToName()
|
||||
<ProviderLabel ProviderType="@provider" CustomIconDataUrl="@(provider == this.DataLLMProvider ? this.DataCustomIconDataUrl : string.Empty)" Text="@provider.ToName()" />
|
||||
</MudSelectItem>
|
||||
}
|
||||
}
|
||||
@ -163,4 +163,4 @@
|
||||
}
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
|
||||
@ -56,6 +56,12 @@ public partial class TranscriptionProviderDialog : MSGComponentBase, ISecretId
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public LLMProviders DataLLMProvider { get; set; } = LLMProviders.NONE;
|
||||
|
||||
/// <summary>
|
||||
/// The validated custom icon supplied by a configuration plugin.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string DataCustomIconDataUrl { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,8 +393,20 @@
|
||||
<ThirdPartyComponent Name="ReverseMarkdown" Developer="Babu Annamalai & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/mysticmind/reversemarkdown-net/blob/master/LICENSE" RepositoryUrl="https://github.com/mysticmind/reversemarkdown-net" UseCase="@T("This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant.")"/>
|
||||
<ThirdPartyComponent Name="wikEd diff" Developer="Cacycle & Open Source Community" LicenseName="None (public domain)" LicenseUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff#License" RepositoryUrl="https://en.wikipedia.org/wiki/User:Cacycle/diff" UseCase="@T("This library is used to display the differences between two texts. This is necessary, e.g., for the grammar and spelling assistant.")"/>
|
||||
<ThirdPartyComponent Name="CodeJar" Developer="Anton Medvedev" LicenseName="MIT" LicenseUrl="https://github.com/antonmedv/codejar/blob/master/LICENSE" RepositoryUrl="https://github.com/antonmedv/codejar/" UseCase="@T("CodeJar is a lightweight embeddable code editor for the browser.")"/>
|
||||
<ThirdPartyComponent Name="Simple Icons" Developer="Simple Icons Contributors & Open Source Community" LicenseName="CC0-1.0" LicenseUrl="https://github.com/simple-icons/simple-icons/blob/16.21.0/LICENSE.md" RepositoryUrl="https://github.com/simple-icons/simple-icons" UseCase="@T("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.")"/>
|
||||
</MudGrid>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Copyright" HeaderText="@T("Trademarks & Brand Assets")">
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@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.")
|
||||
</MudJustifiedText>
|
||||
<MudJustifiedText Typo="Typo.body1" Class="mb-3">
|
||||
@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.")
|
||||
</MudJustifiedText>
|
||||
<MudJustifiedText Typo="Typo.body1">
|
||||
@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.")
|
||||
</MudJustifiedText>
|
||||
</ExpansionPanel>
|
||||
<ExpansionPanel HeaderIcon="@Icons.Material.Filled.Verified" HeaderText="License: FSL-1.1-MIT">
|
||||
<MudMarkdown Value="@LICENSE" Props="Markdown.DefaultConfig" MarkdownPipeline="Markdown.SAFE_MARKDOWN_PIPELINE"/>
|
||||
</ExpansionPanel>
|
||||
|
||||
@ -63,7 +63,10 @@
|
||||
<MudTd>
|
||||
<MudAvatar Size="Size.Medium" Style="width: 2em; height: 2em;" Class="align-content-stretch">
|
||||
<div class="plugin-icon-container">
|
||||
@((MarkupString)context.IconSVG)
|
||||
@if (!string.IsNullOrEmpty(context.IconDataUrl))
|
||||
{
|
||||
<img src="@context.IconDataUrl" alt="" aria-hidden="true" draggable="false" />
|
||||
}
|
||||
</div>
|
||||
</MudAvatar>
|
||||
</MudTd>
|
||||
|
||||
@ -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"] = "<https address of the server>",
|
||||
--
|
||||
-- -- 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:<base64-encoded encrypted data>",
|
||||
--
|
||||
@ -173,6 +190,10 @@ CONFIG["EMBEDDING_PROVIDERS"] = {}
|
||||
-- ["Host"] = "OLLAMA",
|
||||
-- ["Hostname"] = "<https address of the server>",
|
||||
--
|
||||
-- -- 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:<base64-encoded encrypted data>",
|
||||
--
|
||||
|
||||
@ -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:"
|
||||
|
||||
|
||||
@ -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:"
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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<EmbeddingProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<EmbeddingProvider>();
|
||||
|
||||
@ -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<string>(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<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(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
|
||||
|
||||
125
app/MindWork AI Studio/Settings/PluginIconFile.cs
Normal file
@ -0,0 +1,125 @@
|
||||
namespace AIStudio.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Loads an icon file a configuration plugin points to, such as a custom provider logo.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -22,6 +22,7 @@ namespace AIStudio.Settings;
|
||||
/// <param name="Hostname">The hostname of the provider. Useful for self-hosted providers.</param>
|
||||
/// <param name="Model">The LLM model to use for chat.</param>
|
||||
/// <param name="AllowUserProvidedAPIKey">When set by a configuration plugin, the user may set their own API key for this otherwise locked, enterprise-managed provider.</param>
|
||||
/// <param name="CustomIconDataUrl">The validated custom SVG icon supplied by a configuration plugin.</param>
|
||||
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<Provider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<Provider>();
|
||||
|
||||
@ -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<string>(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<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(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
|
||||
|
||||
@ -660,6 +660,29 @@ public sealed class SettingsManager
|
||||
.ThenBy(x => x.Num)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the embedding provider with the given id, without applying any confidence filtering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="embeddingProviderId">The id of the embedding provider to look up.</param>
|
||||
/// <returns>The embedding provider, or EmbeddingProvider.NONE when no embedding provider with that id exists.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all configured transcription providers.
|
||||
/// </summary>
|
||||
@ -675,6 +698,29 @@ public sealed class SettingsManager
|
||||
.ThenBy(x => x.Num)
|
||||
.ToList();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the transcription provider with the given id, without applying any confidence filtering.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="transcriptionProviderId">The id of the transcription provider to look up.</param>
|
||||
/// <returns>The transcription provider, or TranscriptionProvider.NONE when no transcription provider with that id exists.</returns>
|
||||
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);
|
||||
|
||||
@ -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<TranscriptionProvider> LOGGER = Program.LOGGER_FACTORY.CreateLogger<TranscriptionProvider>();
|
||||
|
||||
@ -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<string>(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<bool>(out var allowUserProvidedApiKeyBool))
|
||||
allowUserProvidedApiKey = allowUserProvidedApiKeyBool;
|
||||
|
||||
var customIconDataUrl = string.Empty;
|
||||
if (table.TryGetValue("IconPath", out var iconPathValue))
|
||||
{
|
||||
if (!iconPathValue.TryRead<string>(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
|
||||
|
||||
@ -3,9 +3,14 @@ namespace AIStudio.Tools.PluginSystem;
|
||||
public interface IPluginMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The icon of this plugin.
|
||||
/// The icon of this plugin, as a data URL ready for the src attribute of an image element.
|
||||
/// </summary>
|
||||
public string IconSVG { get; }
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public string IconDataUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of this plugin.
|
||||
|
||||
@ -7,39 +7,62 @@ public abstract partial class PluginBase
|
||||
<svg height="1.5em" width="1.5em" viewBox="0 0 24 24" fill="#1f1f1f"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 13h-2V7h-6V5c0-.28-.22-.5-.5-.5s-.5.22-.5.5v2H4l.01 2.12C5.76 9.8 7 11.51 7 13.5c0 1.99-1.25 3.7-3 4.38V20h2.12c.68-1.75 2.39-3 4.38-3 1.99 0 3.7 1.25 4.38 3H17v-6h2c.28 0 .5-.22.5-.5s-.22-.5-.5-.5z" opacity=".3"/><path d="M19 11V7c0-1.1-.9-2-2-2h-4c0-1.38-1.12-2.5-2.5-2.5S8 3.62 8 5H4c-1.1 0-1.99.9-1.99 2v3.8h.29c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-.3c0-1.49 1.21-2.7 2.7-2.7s2.7 1.21 2.7 2.7v.3H17c1.1 0 2-.9 2-2v-4c1.38 0 2.5-1.12 2.5-2.5S20.38 11 19 11zm0 3h-2v6h-2.12c-.68-1.75-2.39-3-4.38-3-1.99 0-3.7 1.25-4.38 3H4v-2.12c1.75-.68 3-2.39 3-4.38 0-1.99-1.24-3.7-2.99-4.38L4 7h6V5c0-.28.22-.5.5-.5s.5.22.5.5v2h6v6h2c.28 0 .5.22.5.5s-.22.5-.5.5z"/></svg>
|
||||
""";
|
||||
|
||||
private static readonly string DEFAULT_ICON_DATA_URL = CreateDefaultIconDataUrl();
|
||||
|
||||
#region Initialization-related methods
|
||||
|
||||
/// <summary>
|
||||
/// Tries to initialize the icon of the plugin.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When no icon is specified, the default icon will be used.
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="message">The error message, when the icon could not be read.</param>
|
||||
/// <param name="iconSVG">The read icon as SVG.</param>
|
||||
/// <param name="iconDataUrl">The read icon as a data URL.</param>
|
||||
/// <returns>True, when the icon could be read successfully.</returns>
|
||||
|
||||
// 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<string>(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
|
||||
}
|
||||
@ -16,8 +16,8 @@ public abstract partial class PluginBase : IPluginMetadata, IDisposable
|
||||
protected readonly List<string> PluginIssues = [];
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IconSVG { get; }
|
||||
|
||||
public string IconDataUrl { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
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))
|
||||
{
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ public sealed class PluginMetadata(PluginBase plugin, string localPath, bool isM
|
||||
#region Implementation of IPluginMetadata
|
||||
|
||||
/// <inheritdoc />
|
||||
public string IconSVG { get; } = plugin.IconSVG;
|
||||
public string IconDataUrl { get; } = plugin.IconDataUrl;
|
||||
|
||||
/// <inheritdoc />
|
||||
public PluginType Type { get; } = plugin.Type;
|
||||
|
||||
113
app/MindWork AI Studio/Tools/SvgIcon.cs
Normal file
@ -0,0 +1,113 @@
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
/// <summary>
|
||||
/// Turns an SVG icon which came from outside the app into a data URL we can put into an img tag.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
internal static class SvgIcon
|
||||
{
|
||||
/// <summary>
|
||||
/// The largest icon we accept.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Validates the given SVG and converts it into a data URL.
|
||||
/// </summary>
|
||||
/// <param name="svg">The SVG markup to convert.</param>
|
||||
/// <param name="dataUrl">The resulting data URL, or an empty string when the icon was rejected.</param>
|
||||
/// <param name="issue">The reason why the icon was rejected, or an empty string on success.</param>
|
||||
/// <returns>True, when the icon could be converted.</returns>
|
||||
public static bool TryCreateDataUrl(string svg, out string dataUrl, out string issue) => TryCreateDataUrl(Encoding.UTF8.GetBytes(svg), out dataUrl, out issue);
|
||||
|
||||
/// <inheritdoc cref="TryCreateDataUrl(string,out string,out string)"/>
|
||||
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)}";
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FF6A00" d="M3.996 4.517h5.291L8.01 6.324 4.153 7.506a1.668 1.668 0 0 0-1.165 1.601v5.786a1.668 1.668 0 0 0 1.165 1.6l3.857 1.183 1.277 1.807H3.996A3.996 3.996 0 0 1 0 15.487V8.513a3.996 3.996 0 0 1 3.996-3.996m16.008 0h-5.291l1.277 1.807 3.857 1.182c.715.227 1.17.889 1.165 1.601v5.786a1.668 1.668 0 0 1-1.165 1.6l-3.857 1.183-1.277 1.807h5.291A3.996 3.996 0 0 0 24 15.487V8.513a3.996 3.996 0 0 0-3.996-3.996m-4.007 8.345H8.002v-1.804h7.995Z"/></svg>
|
||||
|
After Width: | Height: | Size: 524 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFFFFF" d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#191919" d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"/></svg>
|
||||
|
After Width: | Height: | Size: 262 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#5786FE" d="M23.748 4.651c-.254-.124-.364.113-.512.233-.051.04-.094.09-.137.137-.372.397-.806.657-1.373.626-.829-.046-1.537.214-2.163.848-.133-.782-.575-1.248-1.247-1.548-.352-.155-.708-.311-.955-.65-.172-.24-.219-.509-.305-.774-.055-.16-.11-.323-.293-.35-.2-.031-.278.136-.356.276-.313.572-.434 1.202-.422 1.84.027 1.436.633 2.58 1.838 3.393.137.094.172.187.129.323-.082.28-.18.553-.266.833-.055.179-.137.218-.328.14a5.5 5.5 0 0 1-1.737-1.179c-.857-.828-1.631-1.743-2.597-2.46a12 12 0 0 0-.689-.47c-.985-.957.13-1.743.387-1.836.27-.098.094-.433-.778-.428-.872.003-1.67.295-2.687.685a3 3 0 0 1-.465.136 9.6 9.6 0 0 0-2.883-.101c-1.885.21-3.39 1.1-4.497 2.622C.082 8.776-.231 10.854.152 13.02c.403 2.284 1.568 4.175 3.36 5.653 1.857 1.533 3.997 2.284 6.438 2.14 1.482-.085 3.132-.284 4.994-1.86.47.234.962.328 1.78.398.629.058 1.235-.031 1.705-.129.735-.155.684-.836.418-.961-2.155-1.004-1.682-.595-2.112-.926 1.095-1.295 2.768-3.598 3.284-6.733.05-.346.115-.834.108-1.114-.004-.171.035-.238.23-.257a4.2 4.2 0 0 0 1.545-.475c1.397-.763 1.96-2.016 2.093-3.517.02-.23-.004-.467-.247-.588M11.58 18.168c-2.088-1.642-3.101-2.183-3.52-2.16-.39.024-.32.472-.234.763.09.288.207.487.371.74.114.167.192.416-.113.603-.673.416-1.842-.14-1.897-.168-1.361-.801-2.5-1.86-3.301-3.306-.775-1.393-1.225-2.888-1.299-4.482-.02-.385.094-.522.477-.592a4.7 4.7 0 0 1 1.53-.038c2.131.311 3.946 1.264 5.467 2.774.868.86 1.525 1.887 2.202 2.89.72 1.066 1.494 2.082 2.48 2.915.348.291.626.513.892.677-.802.09-2.14.109-3.055-.615zm1.001-6.44a.306.306 0 0 1 .415-.287.3.3 0 0 1 .113.074.3.3 0 0 1 .086.214c0 .17-.136.307-.308.307a.303.303 0 0 1-.306-.307m3.11 1.596c-.2.081-.4.151-.591.16a1.25 1.25 0 0 1-.798-.254c-.274-.23-.47-.358-.551-.758a1.7 1.7 0 0 1 .015-.588c.07-.327-.007-.537-.238-.727-.188-.156-.426-.199-.689-.199a.6.6 0 0 1-.254-.078.253.253 0 0 1-.114-.358 1 1 0 0 1 .192-.21c.356-.202.767-.136 1.146.016.352.144.618.408 1.001.782.392.451.462.576.685.915.176.264.336.536.446.848.066.194-.02.353-.25.45"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path fill="#6720FF" d="M15.9851 19.1274c-.8882 0-1.6852-.5273-2.0251-1.3436L9.8626 8h2.3982l3.7383 8.9499L19.7339 8h2.3982l-4.1219 9.7873c-.3416.8128-1.1369 1.3401-2.0251 1.3401Zm5.3465 4.6755c-.8847 0-1.6783-.5237-2.0216-1.3331-.3451-.8163-.1664-1.7483.4572-2.3807l7.4627-7.5594.9319 2.1985-6.832 6.9073 9.7382-.0543L32 23.7797l-10.6667.0263-.0035-.0035h.0018ZM0 23.7766l.932-2.1985 9.7382.0543-6.8303-6.909.932-2.1985 7.4626 7.5589c.6237.6307.8041 1.5662.4573 2.3807-.3434.8111-1.1405 1.3332-2.0216 1.3332L.0035 23.7731Z"/></svg>
|
||||
|
After Width: | Height: | Size: 593 B |
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Gemini logo</title>
|
||||
<desc id="desc">A four-point curved star with a red, yellow, green, and blue gradient on a transparent background.</desc>
|
||||
|
||||
<defs>
|
||||
<path id="spark" d="M 255.5 20.5 C 256.83 20.5 258.92 21.42 260 22 C 261.08 22.58 261.17 23 262 24 C 262.83 25 264.33 26.67 265 28 C 265.67 29.33 265.5 30 266 32 C 266.5 34 267 37 268 40 C 269 43 270.5 46 272 50 C 273.5 54 275 59 277 64 C 279 69 281.17 74 284 80 C 286.83 86 290.33 93.33 294 100 C 297.67 106.67 301.67 113.33 306 120 C 310.33 126.67 314.83 133.33 320 140 C 325.17 146.67 330.67 153.33 337 160 C 343.33 166.67 350.17 173.33 358 180 C 365.83 186.67 375.5 194 384 200 C 392.5 206 400.17 211 409 216 C 417.83 221 427.83 226 437 230 C 446.17 234 457.33 237.67 464 240 C 470.67 242.33 473.33 242.67 477 244 C 480.67 245.33 484 246.67 486 248 C 488 249.33 488.25 250.75 489 252 C 489.75 253.25 490.5 254.17 490.5 255.5 C 490.5 256.83 489.58 258.92 489 260 C 488.42 261.08 488 261.17 487 262 C 486 262.83 484.33 264.33 483 265 C 481.67 265.67 481 265.5 479 266 C 477 266.5 474 267 471 268 C 468 269 465 270.5 461 272 C 457 273.5 452 275 447 277 C 442 279 437 281.17 431 284 C 425 286.83 417.67 290.33 411 294 C 404.33 297.67 397.67 301.67 391 306 C 384.33 310.33 377.67 314.83 371 320 C 364.33 325.17 357.67 330.67 351 337 C 344.33 343.33 337.67 350.17 331 358 C 324.33 365.83 317 375.5 311 384 C 305 392.5 300 400.17 295 409 C 290 417.83 285 427.83 281 437 C 277 446.17 273.33 457.33 271 464 C 268.67 470.67 268.33 473.33 267 477 C 265.67 480.67 264.33 484 263 486 C 261.67 488 260.25 488.25 259 489 C 257.75 489.75 256.83 490.5 255.5 490.5 C 254.17 490.5 252.08 489.58 251 489 C 249.92 488.42 249.83 488 249 487 C 248.17 486 246.67 484.33 246 483 C 245.33 481.67 245.5 481 245 479 C 244.5 477 244 474 243 471 C 242 468 240.5 465 239 461 C 237.5 457 236 452 234 447 C 232 442 229.83 437 227 431 C 224.17 425 220.67 417.67 217 411 C 213.33 404.33 209.33 397.67 205 391 C 200.67 384.33 196.17 377.67 191 371 C 185.83 364.33 180.33 357.67 174 351 C 167.67 344.33 160.83 337.67 153 331 C 145.17 324.33 135.5 317 127 311 C 118.5 305 110.83 300 102 295 C 93.17 290 83.17 285 74 281 C 64.83 277 53.67 273.33 47 271 C 40.33 268.67 37.67 268.33 34 267 C 30.33 265.67 27 264.33 25 263 C 23 261.67 22.75 260.25 22 259 C 21.25 257.75 20.5 256.83 20.5 255.5 C 20.5 254.17 21.42 252.08 22 251 C 22.58 249.92 23 249.83 24 249 C 25 248.17 26.67 246.67 28 246 C 29.33 245.33 30 245.5 32 245 C 34 244.5 37 244 40 243 C 43 242 46 240.5 50 239 C 54 237.5 59 236 64 234 C 69 232 74 229.83 80 227 C 86 224.17 93.33 220.67 100 217 C 106.67 213.33 113.33 209.33 120 205 C 126.67 200.67 133.33 196.17 140 191 C 146.67 185.83 153.33 180.33 160 174 C 166.67 167.67 173.33 160.83 180 153 C 186.67 145.17 194 135.5 200 127 C 206 118.5 211 110.83 216 102 C 221 93.17 226 83.17 230 74 C 234 64.83 237.67 53.67 240 47 C 242.33 40.33 242.67 37.67 244 34 C 245.33 30.33 246.67 27 248 25 C 249.33 23 250.75 22.75 252 22 C 253.25 21.25 254.17 20.5 255.5 20.5 Z"/>
|
||||
|
||||
<linearGradient id="vertical" x1="0" y1="20" x2="0" y2="491" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FA4342"/>
|
||||
<stop offset="0.10" stop-color="#F64848"/>
|
||||
<stop offset="0.21" stop-color="#DD586B"/>
|
||||
<stop offset="0.31" stop-color="#A476AE"/>
|
||||
<stop offset="0.42" stop-color="#628AE6"/>
|
||||
<stop offset="0.51" stop-color="#3F8FF2"/>
|
||||
<stop offset="0.64" stop-color="#3B9AD3"/>
|
||||
<stop offset="0.81" stop-color="#30B18C"/>
|
||||
<stop offset="1" stop-color="#0ABA62"/>
|
||||
</linearGradient>
|
||||
|
||||
<radialGradient id="warm" cx="145" cy="135" r="230" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F34A47" stop-opacity="0.84"/>
|
||||
<stop offset="0.45" stop-color="#F34A47" stop-opacity="0.52"/>
|
||||
<stop offset="0.88" stop-color="#F34A47" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="yellow" cx="21" cy="256" r="235" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FFC414"/>
|
||||
<stop offset="0.35" stop-color="#FFC414" stop-opacity="0.92"/>
|
||||
<stop offset="0.72" stop-color="#C9D72E" stop-opacity="0.45"/>
|
||||
<stop offset="1" stop-color="#C9D72E" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="blue" cx="491" cy="256" r="365" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#3186FF"/>
|
||||
<stop offset="0.38" stop-color="#3186FF" stop-opacity="0.96"/>
|
||||
<stop offset="0.72" stop-color="#3186FF" stop-opacity="0.55"/>
|
||||
<stop offset="1" stop-color="#3186FF" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<radialGradient id="green" cx="256" cy="491" r="240" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#08B966"/>
|
||||
<stop offset="0.38" stop-color="#10B66B" stop-opacity="0.72"/>
|
||||
<stop offset="0.72" stop-color="#22B47B" stop-opacity="0.24"/>
|
||||
<stop offset="1" stop-color="#22B47B" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
|
||||
<clipPath id="sparkClip">
|
||||
<use href="#spark"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
|
||||
<g clip-path="url(#sparkClip)">
|
||||
<rect width="512" height="512" fill="url(#vertical)"/>
|
||||
<rect width="512" height="512" fill="url(#blue)"/>
|
||||
<rect width="512" height="512" fill="url(#warm)"/>
|
||||
<rect width="512" height="512" fill="url(#yellow)"/>
|
||||
<rect width="512" height="512" fill="url(#green)"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33 33"><path fill="#F43E01" d="M.54.39h32v32h-32z"/><path fill="#FFFFFF" d="m18.445 4.406-9.468 13.74 7.341.665-1.69 9.578 9.469-13.74-7.342-.664 1.69-9.579Z"/></svg>
|
||||
|
After Width: | Height: | Size: 220 B |
@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="233"
|
||||
height="214"
|
||||
viewBox="0 0 233 214"
|
||||
role="img"
|
||||
aria-labelledby="title desc">
|
||||
<title id="title">GWDG Digitalmedien – blaues Signet</title>
|
||||
<desc id="desc">Aus der gelieferten PNG-Vorlage extrahiertes blaues Signet ohne graues Bogenelement.</desc>
|
||||
<path fill="#009EE0" d="
|
||||
M 82 0
|
||||
H 128
|
||||
C 138 0 146 8 146 18
|
||||
V 64
|
||||
C 146 74 138 82 128 82
|
||||
H 84
|
||||
C 75 82 67 90 67 99
|
||||
V 128
|
||||
C 67 137 75 145 84 145
|
||||
H 95
|
||||
C 104 145 111 138 111 129
|
||||
V 114
|
||||
C 111 105 119 97 128 97
|
||||
H 144
|
||||
C 153 97 161 105 161 114
|
||||
V 128
|
||||
C 161 137 168 144 178 144
|
||||
H 216
|
||||
C 225 144 233 152 233 161
|
||||
V 197
|
||||
C 233 206 225 214 216 214
|
||||
H 178
|
||||
C 168 214 161 206 161 197
|
||||
V 165
|
||||
C 161 155 154 148 144 148
|
||||
H 131
|
||||
C 122 148 114 156 114 165
|
||||
V 179
|
||||
C 114 188 107 196 97 196
|
||||
H 80
|
||||
C 71 196 63 188 63 179
|
||||
V 163
|
||||
C 63 154 56 146 47 146
|
||||
H 17
|
||||
C 8 146 0 138 0 129
|
||||
V 97
|
||||
C 0 87 8 79 17 79
|
||||
H 47
|
||||
C 56 79 64 71 64 62
|
||||
V 18
|
||||
C 64 8 72 0 82 0
|
||||
Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="119" height="103" viewBox="0 0 119 103" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Blablador Icon</title>
|
||||
<desc id="desc">Schwarzes, stilisiertes Gesichtssymbol auf weiß gefüllter Iconfläche; der Außenbereich ist transparent.</desc>
|
||||
|
||||
<!-- Die Iconfläche ist weiß; außerhalb dieses Pfads bleibt die SVG transparent. -->
|
||||
<path
|
||||
d="M36 7.5H84.5C94.2 7.5 102 15.3 102 25V91.5L85 79H35C25.1 79 17 70.9 17 61V25.5C17 15.6 25.1 7.5 35 7.5H36Z"
|
||||
fill="#fff"
|
||||
stroke="#000"
|
||||
stroke-width="5"
|
||||
stroke-linejoin="miter"
|
||||
/>
|
||||
|
||||
<!-- Innere Falz/Trennlinie -->
|
||||
<path d="M85 31V79" fill="none" stroke="#000" stroke-width="5" stroke-linecap="butt" />
|
||||
|
||||
<!-- Gesicht -->
|
||||
<circle cx="36.5" cy="30.5" r="6.75" fill="#000" />
|
||||
<circle cx="67.5" cy="30.5" r="6.75" fill="#000" />
|
||||
<path d="M42.5 45.5H61V51H54.5V59.5H49V51H42.5Z" fill="#000" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 932 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#D50C2D" d="M0 0v24h24V0H0zm4.602 4.025h2.244c.509 0 .716.215.716.717v5.64h8.883v-5.64c0-.509.215-.717.717-.717h2.229c.5 0 .71.23.724.717v14.516c0 .509-.215.717-.717.717h-2.23c-.51 0-.717-.215-.717-.717v-5.735H7.562v5.735c0 .516-.215.717-.716.717H4.602c-.51 0-.717-.208-.717-.717V4.742c0-.509.207-.717.717-.717z"/></svg>
|
||||
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 34 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FA520F" d="M17.143 3.429v3.428h-3.429v3.429h-3.428V6.857H6.857V3.43H3.43v13.714H0v3.428h10.286v-3.428H6.857v-3.429h3.429v3.429h3.429v-3.429h3.428v3.429h-3.428v3.428H24v-3.428h-3.43V3.429z"/></svg>
|
||||
|
After Width: | Height: | Size: 270 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFFFFF" d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#000000" d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 401.25678 293.71975">
|
||||
<path fill="#7624F4" d="M303.9475,17.19926c42.79734,0,77.48933,34.69327,77.48933,77.48933s-34.69199,77.48933-77.48933,77.48933l76.86166,76.86244c9.76367,9.76313,2.84903,26.45667-10.95697,26.45667h-220.88335c-71.32686,0-129.14889-57.82202-129.14889-129.14889S77.64197,17.19926,148.96884,17.19926h154.97866ZM148.96884,68.85881c-42.79607,0-77.48933,34.69327-77.48933,77.48933s34.69327,77.48933,77.48933,77.48933,77.48933-34.69327,77.48933-77.48933-34.69327-77.48933-77.48933-77.48933Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#1FB8CD" d="M22.3977 7.0896h-2.3106V.0676l-7.5094 6.3542V.1577h-1.1554v6.1966L4.4904 0v7.0896H1.6023v10.3976h2.8882V24l6.932-6.3591v6.2005h1.1554v-6.0469l6.9318 6.1807v-6.4879h2.8882V7.0896zm-3.4657-4.531v4.531h-5.355l5.355-4.531zm-13.2862.0676 4.8691 4.4634H5.6458V2.6262zM2.7576 16.332V8.245h7.8476l-6.1149 6.1147v1.9723H2.7576zm2.8882 5.0404v-3.8852h.0001v-2.6488l5.7763-5.7764v7.0111l-5.7764 5.2993zm12.7086.0248-5.7766-5.1509V9.0618l5.7766 5.7766v6.5588zm2.8882-5.0652h-1.733v-1.9723L13.3948 8.245h7.8478v8.087z"/></svg>
|
||||
|
After Width: | Height: | Size: 598 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#E8EAED" d="M5 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6m14 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6M5 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6m14 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6M8 5h8v2H8zm0 12h8v2H8zM4 9h2v6H4zm14 0h2v6h-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#5F6368" d="M5 3a3 3 0 1 0 0 6 3 3 0 0 0 0-6m14 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6M5 15a3 3 0 1 0 0 6 3 3 0 0 0 0-6m14 0a3 3 0 1 0 0 6 3 3 0 0 0 0-6M8 5h8v2H8zm0 12h8v2H8zM4 9h2v6H4zm14 0h2v6h-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 272 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#E8EAED" d="M4 3h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m0 10h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2m2-7v2h2V6zm0 10v2h2v-2zm4-10v2h8V6zm0 10v2h8v-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#5F6368" d="M4 3h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2m0 10h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2m2-7v2h2V6zm0 10v2h2v-2zm4-10v2h8V6zm0 10v2h8v-2z"/></svg>
|
||||
|
After Width: | Height: | Size: 283 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#FFFFFF" d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"/></svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#000000" d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"/></svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@ -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.
|
||||
|
||||