2026-02-20 15:32:54 +01:00
using System.Globalization ;
2025-01-05 15:11:15 +01:00
using AIStudio.Dialogs ;
2026-02-20 15:32:54 +01:00
using AIStudio.Provider ;
2025-01-05 15:11:15 +01:00
using AIStudio.Settings ;
2026-09-09 18:43:37 +02:00
using AIStudio.Tools.Services ;
2025-01-05 15:11:15 +01:00
using Microsoft.AspNetCore.Components ;
using DialogOptions = AIStudio . Dialogs . DialogOptions ;
namespace AIStudio.Components.Settings ;
2026-02-07 22:59:41 +01:00
public partial class SettingsPanelEmbeddings : SettingsPanelProviderBase
2025-01-05 15:11:15 +01:00
{
2026-09-09 18:43:37 +02:00
[Inject]
private DataSourceEmbeddingService DataSourceEmbeddingService { get ; init ; } = null !;
2026-08-12 21:57:58 +02: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 12:46:10 +02:00
Selector = provider => provider . UsedLLMProvider ,
2026-08-12 21:57:58 +02:00
};
2025-01-05 15:11:15 +01:00
[Parameter]
public List < ConfigurationSelectData < string >> AvailableEmbeddingProviders { get ; set ; } = new ();
[Parameter]
public EventCallback < List < ConfigurationSelectData < string >>> AvailableEmbeddingProvidersChanged { get ; set ; }
2026-01-18 17:15:18 +01:00
private string GetEmbeddingProviderModelName ( EmbeddingProvider provider )
2025-01-05 15:11:15 +01:00
{
2026-01-18 17:15:18 +01:00
// For system models, return localized text:
if ( provider . Model . IsSystemModel )
return T ( "Uses the provider-configured model" );
2025-01-05 15:11:15 +01: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 19:51:26 +01:00
#region Overrides of ComponentBase
protected override async Task OnInitializedAsync ()
{
await this . UpdateEmbeddingProviders ();
await base . OnInitializedAsync ();
}
#endregion
2025-01-05 15:11:15 +01:00
private async Task AddEmbeddingProvider ()
{
var dialogParameters = new DialogParameters < EmbeddingProviderDialog >
{
{ x => x . IsEditing , false },
};
2025-05-11 16:57:52 +02:00
var dialogReference = await this . DialogService . ShowAsync < EmbeddingProviderDialog >( T ( "Add Embedding Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01: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 18:43:37 +02:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ();
2025-01-05 15:11:15 +01:00
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
private async Task EditEmbeddingProvider ( EmbeddingProvider embeddingProvider )
{
2026-08-15 11:20:36 +02:00
if ( embeddingProvider . IsEnterpriseConfiguration && ! embeddingProvider . AllowUserProvidedAPIKey )
return ;
2025-01-05 15:11:15 +01: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 12:46:10 +02:00
{ x => x . DataCustomIconDataUrl , embeddingProvider . CustomIconDataUrl },
2025-01-05 15:11:15 +01: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 18:43:37 +02:00
{ x => x . DataTokenizerPath , embeddingProvider . TokenizerPath },
{ x => x . DataTokenLimit , embeddingProvider . EffectiveTokenLimit },
{ x => x . DataEmbeddingBatchSize , embeddingProvider . EffectiveEmbeddingBatchSize },
2026-08-30 16:45:14 +02:00
{ x => x . HFInferenceProviderId , embeddingProvider . HFInferenceProvider },
2026-08-15 11:20:36 +02:00
{ x => x . IsEnterpriseConfiguration , embeddingProvider . IsEnterpriseConfiguration },
2025-01-05 15:11:15 +01:00
};
2025-05-11 16:57:52 +02:00
var dialogReference = await this . DialogService . ShowAsync < EmbeddingProviderDialog >( T ( "Edit Embedding Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled )
return ;
2026-08-15 11:20:36 +02: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 15:11:15 +01: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 18:43:37 +02:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ();
2025-01-05 15:11:15 +01:00
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
private async Task DeleteEmbeddingProvider ( EmbeddingProvider provider )
{
2025-08-28 18:51:44 +02:00
var dialogParameters = new DialogParameters < ConfirmDialog >
2025-01-05 15:11:15 +01:00
{
2025-08-28 18:51:44 +02:00
{ x => x . Message , string . Format ( T ( "Are you sure you want to delete the embedding provider '{0}'?" ), provider . Name ) },
2025-01-05 15:11:15 +01:00
};
2025-05-11 16:57:52 +02:00
var dialogReference = await this . DialogService . ShowAsync < ConfirmDialog >( T ( "Delete Embedding Provider" ), dialogParameters , DialogOptions . FULLSCREEN );
2025-01-05 15:11:15 +01:00
var dialogResult = await dialogReference . Result ;
if ( dialogResult is null || dialogResult . Canceled )
return ;
2026-01-11 16:02:28 +01:00
var deleteSecretResponse = await this . RustService . DeleteAPIKey ( provider , SecretStoreType . EMBEDDING_PROVIDER );
2026-09-09 18:43:37 +02: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 15:11:15 +01:00
if ( deleteSecretResponse . Success )
{
this . SettingsManager . ConfigurationData . EmbeddingProviders . Remove ( provider );
await this . SettingsManager . StoreSettings ();
}
2026-09-09 18:43:37 +02: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 16:02:28 +01:00
2025-01-05 15:11:15 +01:00
await this . UpdateEmbeddingProviders ();
2026-09-09 18:43:37 +02:00
await this . DataSourceEmbeddingService . QueueAllInternalDataSourcesAsync ();
2025-01-05 15:11:15 +01:00
await this . MessageBus . SendMessage < bool >( this , Event . CONFIGURATION_CHANGED );
}
2026-02-07 22:59:41 +01:00
private async Task ExportEmbeddingProvider ( EmbeddingProvider provider )
{
2026-02-19 21:31:59 +01:00
if (! this . SettingsManager . ConfigurationData . App . ShowAdminSettings )
return ;
2026-02-07 22:59:41 +01:00
if ( provider == EmbeddingProvider . NONE )
return ;
await this . ExportProvider ( provider , SecretStoreType . EMBEDDING_PROVIDER , provider . ExportAsConfigurationSection );
}
2025-01-05 15:11:15 +01:00
private async Task UpdateEmbeddingProviders ()
{
this . AvailableEmbeddingProviders . Clear ();
2026-08-12 21:57:58 +02:00
foreach ( var provider in this . SettingsManager . GetAllEmbeddingProviders ())
2025-01-05 15:11:15 +01:00
this . AvailableEmbeddingProviders . Add ( new ( provider . Name , provider . Id ));
await this . AvailableEmbeddingProvidersChanged . InvokeAsync ( this . AvailableEmbeddingProviders );
}
2026-02-20 15:32:54 +01: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 18:43:37 +02: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 15:32:54 +01: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 22:59:41 +01:00
}