2026-02-20 14:32:54 +00:00
using System.Globalization ;
2025-01-05 14:11:15 +00:00
using AIStudio.Dialogs ;
2026-02-20 14:32:54 +00:00
using AIStudio.Provider ;
2025-01-05 14:11:15 +00:00
using AIStudio.Settings ;
2026-09-09 16:43:37 +00:00
using AIStudio.Tools.Services ;
2025-01-05 14:11:15 +00:00
using Microsoft.AspNetCore.Components ;
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
namespace AIStudio.Components.Settings ;
2026-02-07 21:59:41 +00:00
public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
2025-01-05 14:11:15 +00:00
{
2026-09-09 16:43:37 +00:00
[Inject]
private DataSourceEmbeddingService DataSourceEmbeddingService { get ; init ; } = null ! ;
2026-08-12 19:57:58 +00:00
/// <summary>
/// Groups the table by the used LLM provider. The embedding provider list is already sorted by
/// that provider, so all instances of one LLM provider form a single, coherent group.
/// </summary>
private static readonly TableGroupDefinition < EmbeddingProvider > GROUP_CONFIG = new ( )
{
Expandable = true ,
IsInitiallyExpanded = false ,
2026-08-25 10:46:10 +00:00
Selector = provider = > provider . UsedLLMProvider ,
2026-08-12 19:57:58 +00:00
} ;
2025-01-05 14:11:15 +00:00
[Parameter]
public List < ConfigurationSelectData < string > > AvailableEmbeddingProviders { get ; set ; } = new ( ) ;
[Parameter]
public EventCallback < List < ConfigurationSelectData < string > > > AvailableEmbeddingProvidersChanged { get ; set ; }
2026-01-18 16:15:18 +00:00
private string GetEmbeddingProviderModelName ( EmbeddingProvider provider )
2025-01-05 14:11:15 +00:00
{
2026-01-18 16:15:18 +00:00
// For system models, return localized text:
if ( provider . Model . IsSystemModel )
return T ( "Uses the provider-configured model" ) ;
2025-01-05 14:11:15 +00:00
const int MAX_LENGTH = 36 ;
var modelName = provider . Model . ToString ( ) ;
return modelName . Length > MAX_LENGTH ? "[...] " + modelName [ ^ Math . Min ( MAX_LENGTH , modelName . Length ) . . ] : modelName ;
}
2025-01-13 18:51:26 +00:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ( )
{
await this . UpdateEmbeddingProviders ( ) ;
await base . OnInitializedAsync ( ) ;
}
#endregion
2025-01-05 14:11:15 +00:00
private async Task AddEmbeddingProvider ( )
{
var dialogParameters = new DialogParameters < EmbeddingProviderDialog >
{
{ x = > x . IsEditing , false } ,
} ;
2025-05-11 14:57:52 +00:00
var dialogReference = await this . DialogService . ShowAsync < EmbeddingProviderDialog > ( T ( "Add Embedding Provider" ) , dialogParameters , DialogOptions . FULLSCREEN ) ;
2025-01-05 14:11:15 +00:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled )
return ;
var addedEmbedding = ( EmbeddingProvider ) dialogResult . Data ! ;
addedEmbedding = addedEmbedding with { Num = this . SettingsManager . ConfigurationData . NextEmbeddingNum + + } ;
this . SettingsManager . ConfigurationData . EmbeddingProviders . Add ( addedEmbedding ) ;
await this . UpdateEmbeddingProviders ( ) ;
await this . SettingsManager . StoreSettings ( ) ;
2026-09-09 16:43:37 +00:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ( ) ;
2025-01-05 14:11:15 +00:00
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
private async Task EditEmbeddingProvider ( EmbeddingProvider embeddingProvider )
{
2026-08-15 09:20:36 +00:00
if ( embeddingProvider . IsEnterpriseConfiguration & & ! embeddingProvider . AllowUserProvidedAPIKey )
return ;
2025-01-05 14:11:15 +00:00
var dialogParameters = new DialogParameters < EmbeddingProviderDialog >
{
{ x = > x . DataNum , embeddingProvider . Num } ,
{ x = > x . DataId , embeddingProvider . Id } ,
{ x = > x . DataName , embeddingProvider . Name } ,
{ x = > x . DataLLMProvider , embeddingProvider . UsedLLMProvider } ,
2026-08-25 10:46:10 +00:00
{ x = > x . DataCustomIconDataUrl , embeddingProvider . CustomIconDataUrl } ,
2025-01-05 14:11:15 +00:00
{ x = > x . DataModel , embeddingProvider . Model } ,
{ x = > x . DataHostname , embeddingProvider . Hostname } ,
{ x = > x . IsSelfHosted , embeddingProvider . IsSelfHosted } ,
{ x = > x . IsEditing , true } ,
{ x = > x . DataHost , embeddingProvider . Host } ,
2026-09-09 16:43:37 +00:00
{ x = > x . DataTokenizerPath , embeddingProvider . TokenizerPath } ,
{ x = > x . DataTokenLimit , embeddingProvider . EffectiveTokenLimit } ,
{ x = > x . DataEmbeddingBatchSize , embeddingProvider . EffectiveEmbeddingBatchSize } ,
2026-08-30 14:45:14 +00:00
{ x = > x . HFInferenceProviderId , embeddingProvider . HFInferenceProvider } ,
2026-08-15 09:20:36 +00:00
{ x = > x . IsEnterpriseConfiguration , embeddingProvider . IsEnterpriseConfiguration } ,
2025-01-05 14:11:15 +00:00
} ;
2025-05-11 14:57:52 +00:00
var dialogReference = await this . DialogService . ShowAsync < EmbeddingProviderDialog > ( T ( "Edit Embedding Provider" ) , dialogParameters , DialogOptions . FULLSCREEN ) ;
2025-01-05 14:11:15 +00:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled )
return ;
2026-08-15 09:20:36 +00:00
if ( embeddingProvider . IsEnterpriseConfiguration )
{
// Only the API key changed, and the dialog already stored it directly. The provider
// object itself is managed by the configuration plugin and must not be overwritten
// with the dialog's copy -- doing so would let the locked-but-technically-editable
// fields drift from what the organization configured.
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
return ;
}
2025-01-05 14:11:15 +00:00
var editedEmbeddingProvider = ( EmbeddingProvider ) dialogResult . Data ! ;
// Set the provider number if it's not set. This is important for providers
// added before we started saving the provider number.
if ( editedEmbeddingProvider . Num = = 0 )
editedEmbeddingProvider = editedEmbeddingProvider with { Num = this . SettingsManager . ConfigurationData . NextEmbeddingNum + + } ;
this . SettingsManager . ConfigurationData . EmbeddingProviders [ this . SettingsManager . ConfigurationData . EmbeddingProviders . IndexOf ( embeddingProvider ) ] = editedEmbeddingProvider ;
await this . UpdateEmbeddingProviders ( ) ;
await this . SettingsManager . StoreSettings ( ) ;
2026-09-09 16:43:37 +00:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ( ) ;
2025-01-05 14:11:15 +00:00
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
private async Task DeleteEmbeddingProvider ( EmbeddingProvider provider )
{
2025-08-28 16:51:44 +00:00
var dialogParameters = new DialogParameters < ConfirmDialog >
2025-01-05 14:11:15 +00:00
{
2025-08-28 16:51:44 +00:00
{ x = > x . Message , string . Format ( T ( "Are you sure you want to delete the embedding provider '{0}'?" ) , provider . Name ) } ,
2025-01-05 14:11:15 +00:00
} ;
2025-05-11 14:57:52 +00:00
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog > ( T ( "Delete Embedding Provider" ) , dialogParameters , DialogOptions . FULLSCREEN ) ;
2025-01-05 14:11:15 +00:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled )
return ;
2026-01-11 15:02:28 +00:00
var deleteSecretResponse = await this . RustService . DeleteAPIKey ( provider , SecretStoreType . EMBEDDING_PROVIDER ) ;
2026-09-09 16:43:37 +00:00
//
// Removing the tokenizer is best effort: it leaves an unused file behind when it fails,
// which is not worth bothering the user about while they are deleting the provider. The
// API key is different, though, because a leftover secret is a secret we promised to remove.
//
_ = await this . RustService . DeleteTokenizer ( TokenizerModelId . ForEmbeddingProvider ( provider ) ) ;
2025-01-05 14:11:15 +00:00
if ( deleteSecretResponse . Success )
{
this . SettingsManager . ConfigurationData . EmbeddingProviders . Remove ( provider ) ;
await this . SettingsManager . StoreSettings ( ) ;
}
2026-09-09 16:43:37 +00:00
else
{
var issueDialogParameters = new DialogParameters < ConfirmDialog >
{
{ x = > x . Message , string . Format ( T ( "Couldn't delete the embedding provider '{0}'. The issue: {1}. We can ignore this issue and delete the embedding provider anyway. Do you want to ignore it and delete this embedding provider?" ) , provider . Name , deleteSecretResponse . Issue ) } ,
} ;
var issueDialogReference = await this . DialogService . ShowAsync < ConfirmDialog > ( T ( "Delete Embedding Provider" ) , issueDialogParameters , DialogOptions . FULLSCREEN ) ;
var issueDialogResult = await issueDialogReference . Result ;
if ( issueDialogResult is null | | issueDialogResult . Canceled )
return ;
this . SettingsManager . ConfigurationData . EmbeddingProviders . Remove ( provider ) ;
await this . SettingsManager . StoreSettings ( ) ;
}
2026-01-11 15:02:28 +00:00
2025-01-05 14:11:15 +00:00
await this . UpdateEmbeddingProviders ( ) ;
2026-09-09 16:43:37 +00:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ( ) ;
2025-01-05 14:11:15 +00:00
await this . MessageBus . SendMessage < bool > ( this , Event . CONFIGURATION_CHANGED ) ;
}
2026-02-07 21:59:41 +00:00
private async Task ExportEmbeddingProvider ( EmbeddingProvider provider )
{
2026-02-19 20:31:59 +00:00
if ( ! this . SettingsManager . ConfigurationData . App . ShowAdminSettings )
return ;
2026-02-07 21:59:41 +00:00
if ( provider = = EmbeddingProvider . NONE )
return ;
await this . ExportProvider ( provider , SecretStoreType . EMBEDDING_PROVIDER , provider . ExportAsConfigurationSection ) ;
}
2025-01-05 14:11:15 +00:00
private async Task UpdateEmbeddingProviders ( )
{
this . AvailableEmbeddingProviders . Clear ( ) ;
2026-08-12 19:57:58 +00:00
foreach ( var provider in this . SettingsManager . GetAllEmbeddingProviders ( ) )
2025-01-05 14:11:15 +00:00
this . AvailableEmbeddingProviders . Add ( new ( provider . Name , provider . Id ) ) ;
await this . AvailableEmbeddingProvidersChanged . InvokeAsync ( this . AvailableEmbeddingProviders ) ;
}
2026-02-20 14:32:54 +00:00
private async Task TestEmbeddingProvider ( EmbeddingProvider provider )
{
var dialogParameters = new DialogParameters < SingleInputDialog >
{
{ x = > x . ConfirmText , T ( "Embed text" ) } ,
{ x = > x . InputHeaderText , T ( "Add text that should be embedded:" ) } ,
{ x = > x . UserInput , T ( "Example text to embed" ) } ,
} ;
var dialogReference = await this . DialogService . ShowAsync < SingleInputDialog > ( T ( "Test Embedding Provider" ) , dialogParameters , DialogOptions . FULLSCREEN ) ;
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null | | dialogResult . Canceled )
return ;
var inputText = dialogResult . Data as string ;
if ( string . IsNullOrWhiteSpace ( inputText ) )
return ;
var embeddingProvider = provider . CreateProvider ( ) ;
2026-09-09 16:43:37 +00:00
IReadOnlyList < IReadOnlyList < float > > embeddings ;
try
{
embeddings = await embeddingProvider . EmbedTextAsync ( provider . Model , this . SettingsManager , CancellationToken . None , inputText ) ;
}
catch ( ProviderRequestException exception )
{
//
// The provider named what went wrong and what to do about it. Showing that beats the
// sentence below, which used to be the same one for a missing API key, an unreachable
// provider and a provider which cannot embed anything at all:
//
await this . DialogService . ShowMessageBox ( T ( "Embedding Result" ) , exception . UserMessage , T ( "Close" ) ) ;
return ;
}
2026-02-20 14:32:54 +00:00
if ( embeddings . Count = = 0 )
{
await this . DialogService . ShowMessageBox ( T ( "Embedding Result" ) , T ( "No embedding was returned." ) , T ( "Close" ) ) ;
return ;
}
var vector = embeddings . FirstOrDefault ( ) ;
if ( vector is null | | vector . Count = = 0 )
{
await this . DialogService . ShowMessageBox ( T ( "Embedding Result" ) , T ( "No embedding was returned." ) , T ( "Close" ) ) ;
return ;
}
var resultText = string . Join ( Environment . NewLine , vector . Select ( value = > value . ToString ( "G9" , CultureInfo . InvariantCulture ) ) ) ;
var resultParameters = new DialogParameters < EmbeddingResultDialog >
{
{ x = > x . ResultText , resultText } ,
{ x = > x . ResultLabel , T ( "Embedding Vector (one dimension per line)" ) } ,
} ;
await this . DialogService . ShowAsync < EmbeddingResultDialog > ( T ( "Embedding Result" ) , resultParameters , DialogOptions . FULLSCREEN ) ;
}
2026-02-07 21:59:41 +00:00
}