feat(llm): change from using precedence list to using a sing specified provider for either chat and/or embeddings

This commit is contained in:
perf3ct
2025-06-04 20:13:13 +00:00
parent f9d8bf26c4
commit a20e36f4ee
15 changed files with 685 additions and 414 deletions

View File

@@ -65,7 +65,7 @@ export default class AiSettingsWidget extends OptionsWidget {
// Core AI options
this.setupChangeHandler('.ai-enabled', 'aiEnabled', true, true);
this.setupChangeHandler('.ai-provider-precedence', 'aiProviderPrecedence', true);
this.setupChangeHandler('.ai-selected-provider', 'aiSelectedProvider', true);
this.setupChangeHandler('.ai-temperature', 'aiTemperature');
this.setupChangeHandler('.ai-system-prompt', 'aiSystemPrompt');
@@ -132,11 +132,28 @@ export default class AiSettingsWidget extends OptionsWidget {
this.setupChangeHandler('.enable-automatic-indexing', 'enableAutomaticIndexing', false, true);
this.setupChangeHandler('.embedding-similarity-threshold', 'embeddingSimilarityThreshold');
this.setupChangeHandler('.max-notes-per-llm-query', 'maxNotesPerLlmQuery');
this.setupChangeHandler('.embedding-provider-precedence', 'embeddingProviderPrecedence', true);
this.setupChangeHandler('.embedding-selected-provider', 'embeddingSelectedProvider', true);
this.setupChangeHandler('.embedding-dimension-strategy', 'embeddingDimensionStrategy');
this.setupChangeHandler('.embedding-batch-size', 'embeddingBatchSize');
this.setupChangeHandler('.embedding-update-interval', 'embeddingUpdateInterval');
// Add provider selection change handlers for dynamic settings visibility
this.$widget.find('.ai-selected-provider').on('change', () => {
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
this.$widget.find('.provider-settings').hide();
if (selectedProvider) {
this.$widget.find(`.${selectedProvider}-provider-settings`).show();
}
});
this.$widget.find('.embedding-selected-provider').on('change', () => {
const selectedProvider = this.$widget.find('.embedding-selected-provider').val() as string;
this.$widget.find('.embedding-provider-settings').hide();
if (selectedProvider) {
this.$widget.find(`.${selectedProvider}-embedding-provider-settings`).show();
}
});
// No sortable behavior needed anymore
// Embedding stats refresh button
@@ -194,42 +211,25 @@ export default class AiSettingsWidget extends OptionsWidget {
return;
}
// Get provider precedence
const providerPrecedence = (this.$widget.find('.ai-provider-precedence').val() as string || '').split(',');
// Get selected provider
const selectedProvider = this.$widget.find('.ai-selected-provider').val() as string;
// Check for OpenAI configuration if it's in the precedence list
const openaiWarnings: string[] = [];
if (providerPrecedence.includes('openai')) {
// Check for selected provider configuration
const providerWarnings: string[] = [];
if (selectedProvider === 'openai') {
const openaiApiKey = this.$widget.find('.openai-api-key').val();
if (!openaiApiKey) {
openaiWarnings.push(t("ai_llm.empty_key_warning.openai"));
providerWarnings.push(t("ai_llm.empty_key_warning.openai"));
}
}
// Check for Anthropic configuration if it's in the precedence list
const anthropicWarnings: string[] = [];
if (providerPrecedence.includes('anthropic')) {
} else if (selectedProvider === 'anthropic') {
const anthropicApiKey = this.$widget.find('.anthropic-api-key').val();
if (!anthropicApiKey) {
anthropicWarnings.push(t("ai_llm.empty_key_warning.anthropic"));
providerWarnings.push(t("ai_llm.empty_key_warning.anthropic"));
}
}
// Check for Voyage configuration if it's in the precedence list
const voyageWarnings: string[] = [];
if (providerPrecedence.includes('voyage')) {
const voyageApiKey = this.$widget.find('.voyage-api-key').val();
if (!voyageApiKey) {
voyageWarnings.push(t("ai_llm.empty_key_warning.voyage"));
}
}
// Check for Ollama configuration if it's in the precedence list
const ollamaWarnings: string[] = [];
if (providerPrecedence.includes('ollama')) {
} else if (selectedProvider === 'ollama') {
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val();
if (!ollamaBaseUrl) {
ollamaWarnings.push(t("ai_llm.ollama_no_url"));
providerWarnings.push(t("ai_llm.ollama_no_url"));
}
}
@@ -238,27 +238,24 @@ export default class AiSettingsWidget extends OptionsWidget {
const embeddingsEnabled = this.$widget.find('.enable-automatic-indexing').prop('checked');
if (embeddingsEnabled) {
const embeddingProviderPrecedence = (this.$widget.find('.embedding-provider-precedence').val() as string || '').split(',');
const selectedEmbeddingProvider = this.$widget.find('.embedding-selected-provider').val() as string;
if (embeddingProviderPrecedence.includes('openai') && !this.$widget.find('.openai-api-key').val()) {
if (selectedEmbeddingProvider === 'openai' && !this.$widget.find('.openai-api-key').val()) {
embeddingWarnings.push(t("ai_llm.empty_key_warning.openai"));
}
if (embeddingProviderPrecedence.includes('voyage') && !this.$widget.find('.voyage-api-key').val()) {
if (selectedEmbeddingProvider === 'voyage' && !this.$widget.find('.voyage-api-key').val()) {
embeddingWarnings.push(t("ai_llm.empty_key_warning.voyage"));
}
if (embeddingProviderPrecedence.includes('ollama') && !this.$widget.find('.ollama-base-url').val()) {
if (selectedEmbeddingProvider === 'ollama' && !this.$widget.find('.ollama-base-url').val()) {
embeddingWarnings.push(t("ai_llm.empty_key_warning.ollama"));
}
}
// Combine all warnings
const allWarnings = [
...openaiWarnings,
...anthropicWarnings,
...voyageWarnings,
...ollamaWarnings,
...providerWarnings,
...embeddingWarnings
];
@@ -449,6 +446,27 @@ export default class AiSettingsWidget extends OptionsWidget {
}
}
/**
* Update provider settings visibility based on selected providers
*/
updateProviderSettingsVisibility() {
if (!this.$widget) return;
// Update AI provider settings visibility
const selectedAiProvider = this.$widget.find('.ai-selected-provider').val() as string;
this.$widget.find('.provider-settings').hide();
if (selectedAiProvider) {
this.$widget.find(`.${selectedAiProvider}-provider-settings`).show();
}
// Update embedding provider settings visibility
const selectedEmbeddingProvider = this.$widget.find('.embedding-selected-provider').val() as string;
this.$widget.find('.embedding-provider-settings').hide();
if (selectedEmbeddingProvider) {
this.$widget.find(`.${selectedEmbeddingProvider}-embedding-provider-settings`).show();
}
}
/**
* Called when the options have been loaded from the server
*/
@@ -459,30 +477,30 @@ export default class AiSettingsWidget extends OptionsWidget {
this.$widget.find('.ai-enabled').prop('checked', options.aiEnabled !== 'false');
this.$widget.find('.ai-temperature').val(options.aiTemperature || '0.7');
this.$widget.find('.ai-system-prompt').val(options.aiSystemPrompt || '');
this.$widget.find('.ai-provider-precedence').val(options.aiProviderPrecedence || 'openai,anthropic,ollama');
this.$widget.find('.ai-selected-provider').val(options.aiSelectedProvider || 'openai');
// OpenAI Section
this.$widget.find('.openai-api-key').val(options.openaiApiKey || '');
this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai_llm.com/v1');
this.$widget.find('.openai-default-model').val(options.openaiDefaultModel || 'gpt-4o');
this.$widget.find('.openai-embedding-model').val(options.openaiEmbeddingModel || 'text-embedding-3-small');
this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai.com/v1');
this.$widget.find('.openai-default-model').val(options.openaiDefaultModel || '');
this.$widget.find('.openai-embedding-model').val(options.openaiEmbeddingModel || '');
// Anthropic Section
this.$widget.find('.anthropic-api-key').val(options.anthropicApiKey || '');
this.$widget.find('.anthropic-base-url').val(options.anthropicBaseUrl || 'https://api.anthropic.com');
this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || 'claude-3-opus-20240229');
this.$widget.find('.anthropic-default-model').val(options.anthropicDefaultModel || '');
// Voyage Section
this.$widget.find('.voyage-api-key').val(options.voyageApiKey || '');
this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || 'voyage-2');
this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || '');
// Ollama Section
this.$widget.find('.ollama-base-url').val(options.ollamaBaseUrl || 'http://localhost:11434');
this.$widget.find('.ollama-default-model').val(options.ollamaDefaultModel || 'llama3');
this.$widget.find('.ollama-embedding-model').val(options.ollamaEmbeddingModel || 'nomic-embed-text');
this.$widget.find('.ollama-default-model').val(options.ollamaDefaultModel || '');
this.$widget.find('.ollama-embedding-model').val(options.ollamaEmbeddingModel || '');
// Embedding Options
this.$widget.find('.embedding-provider-precedence').val(options.embeddingProviderPrecedence || 'openai,voyage,ollama,local');
this.$widget.find('.embedding-selected-provider').val(options.embeddingSelectedProvider || 'openai');
this.$widget.find('.embedding-auto-update-enabled').prop('checked', options.embeddingAutoUpdateEnabled !== 'false');
this.$widget.find('.enable-automatic-indexing').prop('checked', options.enableAutomaticIndexing !== 'false');
this.$widget.find('.embedding-similarity-threshold').val(options.embeddingSimilarityThreshold || '0.75');
@@ -491,6 +509,9 @@ export default class AiSettingsWidget extends OptionsWidget {
this.$widget.find('.embedding-batch-size').val(options.embeddingBatchSize || '10');
this.$widget.find('.embedding-update-interval').val(options.embeddingUpdateInterval || '5000');
// Show/hide provider settings based on selected providers
this.updateProviderSettingsVisibility();
// Display validation warnings
this.displayValidationWarnings();
}

View File

@@ -61,9 +61,125 @@ export const TPL = `
<h4>${t("ai_llm.provider_configuration")}</h4>
<div class="form-group">
<label>${t("ai_llm.provider_precedence")}</label>
<input type="text" class="ai-provider-precedence form-control" placeholder="openai,anthropic,ollama">
<div class="form-text">${t("ai_llm.provider_precedence_description")}</div>
<label>${t("ai_llm.selected_provider")}</label>
<select class="ai-selected-provider form-control">
<option value="">${t("ai_llm.select_provider")}</option>
<option value="openai">OpenAI</option>
<option value="anthropic">Anthropic</option>
<option value="ollama">Ollama</option>
</select>
<div class="form-text">${t("ai_llm.selected_provider_description")}</div>
</div>
<!-- OpenAI Provider Settings -->
<div class="provider-settings openai-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.openai_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="openai-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.openai_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="openai-base-url form-control" />
<div class="form-text">${t("ai_llm.openai_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="openai-default-model form-control">
<option value="gpt-4o">GPT-4o (recommended)</option>
<option value="gpt-4">GPT-4</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
</select>
<div class="form-text">${t("ai_llm.openai_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="openai-embedding-model form-control">
<option value="text-embedding-3-small">text-embedding-3-small (recommended)</option>
<option value="text-embedding-3-large">text-embedding-3-large</option>
</select>
<div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
<!-- Anthropic Provider Settings -->
<div class="provider-settings anthropic-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.anthropic_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="anthropic-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="anthropic-base-url form-control" />
<div class="form-text">${t("ai_llm.anthropic_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="anthropic-default-model form-control">
<option value="claude-3-opus-20240229">Claude 3 Opus (recommended)</option>
<option value="claude-3-sonnet-20240229">Claude 3 Sonnet</option>
<option value="claude-3-haiku-20240307">Claude 3 Haiku</option>
</select>
<div class="form-text">${t("ai_llm.anthropic_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button>
</div>
</div>
</div>
</div>
<!-- Ollama Provider Settings -->
<div class="provider-settings ollama-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.ollama_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="ollama-base-url form-control" />
<div class="form-text">${t("ai_llm.ollama_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="ollama-default-model form-control">
<option value="llama3">llama3 (recommended)</option>
<option value="mistral">mistral</option>
<option value="phi3">phi3</option>
</select>
<div class="form-text">${t("ai_llm.ollama_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="ollama-embedding-model form-control">
<option value="nomic-embed-text">nomic-embed-text (recommended)</option>
<option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option>
</select>
<div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
<div class="form-group">
@@ -79,155 +195,98 @@ export const TPL = `
</div>
</div>
<nav class="options-section-tabs">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
<button class="nav-link active" id="nav-openai-tab" data-bs-toggle="tab" data-bs-target="#nav-openai" type="button" role="tab" aria-controls="nav-openai" aria-selected="true">${t("ai_llm.openai_tab")}</button>
<button class="nav-link" id="nav-anthropic-tab" data-bs-toggle="tab" data-bs-target="#nav-anthropic" type="button" role="tab" aria-controls="nav-anthropic" aria-selected="false">${t("ai_llm.anthropic_tab")}</button>
<button class="nav-link" id="nav-voyage-tab" data-bs-toggle="tab" data-bs-target="#nav-voyage" type="button" role="tab" aria-controls="nav-voyage" aria-selected="false">${t("ai_llm.voyage_tab")}</button>
<button class="nav-link" id="nav-ollama-tab" data-bs-toggle="tab" data-bs-target="#nav-ollama" type="button" role="tab" aria-controls="nav-ollama" aria-selected="false">${t("ai_llm.ollama_tab")}</button>
</div>
</nav>
<div class="options-section">
<div class="tab-content" id="nav-tabContent">
<div class="tab-pane fade show active" id="nav-openai" role="tabpanel" aria-labelledby="nav-openai-tab">
<div class="card">
<div class="card-header">
<h5>${t("ai_llm.openai_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="openai-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.openai_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="openai-base-url form-control" />
<div class="form-text">${t("ai_llm.openai_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="openai-default-model form-control">
<option value="gpt-4o">GPT-4o (recommended)</option>
<option value="gpt-4">GPT-4</option>
<option value="gpt-3.5-turbo">GPT-3.5 Turbo</option>
</select>
<div class="form-text">${t("ai_llm.openai_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-openai-models">${t("ai_llm.refresh_models")}</button>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="openai-embedding-model form-control">
<option value="text-embedding-3-small">text-embedding-3-small (recommended)</option>
<option value="text-embedding-3-large">text-embedding-3-large</option>
</select>
<div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-anthropic" role="tabpanel" aria-labelledby="nav-anthropic-tab">
<div class="card">
<div class="card-header">
<h5>${t("ai_llm.anthropic_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="anthropic-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.anthropic_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="anthropic-base-url form-control" />
<div class="form-text">${t("ai_llm.anthropic_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="anthropic-default-model form-control">
<option value="claude-3-opus-20240229">Claude 3 Opus (recommended)</option>
<option value="claude-3-sonnet-20240229">Claude 3 Sonnet</option>
<option value="claude-3-haiku-20240307">Claude 3 Haiku</option>
</select>
<div class="form-text">${t("ai_llm.anthropic_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-anthropic-models">${t("ai_llm.refresh_models")}</button>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-voyage" role="tabpanel" aria-labelledby="nav-voyage-tab">
<div class="card">
<div class="card-header">
<h5>${t("ai_llm.voyage_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="voyage-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.voyage_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="voyage-embedding-model form-control">
<option value="voyage-2">Voyage-2 (recommended)</option>
<option value="voyage-2-code">Voyage-2-Code</option>
<option value="voyage-large-2">Voyage-Large-2</option>
</select>
<div class="form-text">${t("ai_llm.voyage_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="nav-ollama" role="tabpanel" aria-labelledby="nav-ollama-tab">
<div class="card">
<div class="card-header">
<h5>${t("ai_llm.ollama_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.url")}</label>
<input type="text" class="ollama-base-url form-control" />
<div class="form-text">${t("ai_llm.ollama_url_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.model")}</label>
<select class="ollama-default-model form-control">
<option value="llama3">llama3 (recommended)</option>
<option value="mistral">mistral</option>
<option value="phi3">phi3</option>
</select>
<div class="form-text">${t("ai_llm.ollama_model_description")}</div>
<button class="btn btn-sm btn-outline-secondary refresh-models"><span class="bx bx-refresh"></span></button>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="ollama-embedding-model form-control">
<option value="nomic-embed-text">nomic-embed-text (recommended)</option>
<option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option>
</select>
<div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="options-section">
<h4>${t("ai_llm.embeddings_configuration")}</h4>
<div class="form-group">
<label class="embedding-provider-label">${t("ai_llm.embedding_provider_precedence")}</label>
<input type="text" class="embedding-provider-precedence form-control" placeholder="openai,voyage,ollama,local">
<div class="form-text">${t("ai_llm.embedding_provider_precedence_description")}</div>
<label class="embedding-provider-label">${t("ai_llm.selected_embedding_provider")}</label>
<select class="embedding-selected-provider form-control">
<option value="">${t("ai_llm.select_embedding_provider")}</option>
<option value="openai">OpenAI</option>
<option value="voyage">Voyage AI</option>
<option value="ollama">Ollama</option>
<option value="local">Local</option>
</select>
<div class="form-text">${t("ai_llm.selected_embedding_provider_description")}</div>
</div>
<!-- OpenAI Embedding Provider Settings -->
<div class="embedding-provider-settings openai-embedding-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.openai_embedding_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="openai-embedding-model form-control">
<option value="text-embedding-3-small">text-embedding-3-small (recommended)</option>
<option value="text-embedding-3-large">text-embedding-3-large</option>
</select>
<div class="form-text">${t("ai_llm.openai_embedding_model_description")}</div>
</div>
<div class="form-text text-muted">${t("ai_llm.openai_embedding_shared_settings")}</div>
</div>
</div>
</div>
<!-- Voyage Embedding Provider Settings -->
<div class="embedding-provider-settings voyage-embedding-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.voyage_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.api_key")}</label>
<input type="password" class="voyage-api-key form-control" autocomplete="off" />
<div class="form-text">${t("ai_llm.voyage_api_key_description")}</div>
</div>
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="voyage-embedding-model form-control">
<option value="voyage-2">Voyage-2 (recommended)</option>
<option value="voyage-2-code">Voyage-2-Code</option>
<option value="voyage-large-2">Voyage-Large-2</option>
</select>
<div class="form-text">${t("ai_llm.voyage_embedding_model_description")}</div>
</div>
</div>
</div>
</div>
<!-- Ollama Embedding Provider Settings -->
<div class="embedding-provider-settings ollama-embedding-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.ollama_embedding_settings")}</h5>
</div>
<div class="card-body">
<div class="form-group">
<label>${t("ai_llm.embedding_model")}</label>
<select class="ollama-embedding-model form-control">
<option value="nomic-embed-text">nomic-embed-text (recommended)</option>
<option value="all-MiniLM-L6-v2">all-MiniLM-L6-v2</option>
</select>
<div class="form-text">${t("ai_llm.ollama_embedding_model_description")}</div>
</div>
<div class="form-text text-muted">${t("ai_llm.ollama_embedding_shared_settings")}</div>
</div>
</div>
</div>
<!-- Local Embedding Provider Settings -->
<div class="embedding-provider-settings local-embedding-provider-settings" style="display: none;">
<div class="card mt-3">
<div class="card-header">
<h5>${t("ai_llm.local_embedding_settings")}</h5>
</div>
<div class="card-body">
<div class="form-text">${t("ai_llm.local_embedding_description")}</div>
</div>
</div>
</div>
<div class="form-group">