mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-08-07 16:42:27 +08:00
break up ai_settings.ts into multiple files
This commit is contained in:
parent
58f855a46b
commit
d67e8e61cb
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,932 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { TPL } from "./template.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { FilterOptionsByType, OptionMap } from "../../../../../../services/options_interface.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import type { EmbeddingStats, FailedEmbeddingNotes } from "./interfaces.js";
|
||||
import { ProviderService } from "./providers.js";
|
||||
|
||||
export default class AiSettingsWidget extends OptionsWidget {
|
||||
private ollamaModelsRefreshed = false;
|
||||
private openaiModelsRefreshed = false;
|
||||
private anthropicModelsRefreshed = false;
|
||||
private statsRefreshInterval: NodeJS.Timeout | null = null;
|
||||
private indexRebuildRefreshInterval: NodeJS.Timeout | null = null;
|
||||
private readonly STATS_REFRESH_INTERVAL = 5000; // 5 seconds
|
||||
private providerService: ProviderService | null = null;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.providerService = new ProviderService(this.$widget);
|
||||
|
||||
// Setup event handlers for options
|
||||
this.setupEventHandlers();
|
||||
|
||||
this.refreshEmbeddingStats();
|
||||
this.fetchFailedEmbeddingNotes();
|
||||
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up all event handlers for options
|
||||
*/
|
||||
setupEventHandlers() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// AI Enabled checkbox
|
||||
const $aiEnabled = this.$widget.find('.ai-enabled');
|
||||
$aiEnabled.on('change', async () => {
|
||||
await this.updateOption('aiEnabled', $aiEnabled.prop('checked') ? 'true' : 'false');
|
||||
// Display validation warnings after changing aiEnabled
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
// Provider precedence
|
||||
const $aiProviderPrecedence = this.$widget.find('.ai-provider-precedence');
|
||||
$aiProviderPrecedence.on('change', async () => {
|
||||
await this.updateOption('aiProviderPrecedence', $aiProviderPrecedence.val() as string);
|
||||
// Display validation warnings after changing precedence list
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
// Temperature
|
||||
const $aiTemperature = this.$widget.find('.ai-temperature');
|
||||
$aiTemperature.on('change', async () => {
|
||||
await this.updateOption('aiTemperature', $aiTemperature.val() as string);
|
||||
});
|
||||
|
||||
// System prompt
|
||||
const $aiSystemPrompt = this.$widget.find('.ai-system-prompt');
|
||||
$aiSystemPrompt.on('change', async () => {
|
||||
await this.updateOption('aiSystemPrompt', $aiSystemPrompt.val() as string);
|
||||
});
|
||||
|
||||
// OpenAI options
|
||||
const $openaiApiKey = this.$widget.find('.openai-api-key');
|
||||
$openaiApiKey.on('change', async () => {
|
||||
await this.updateOption('openaiApiKey', $openaiApiKey.val() as string);
|
||||
// Display validation warnings after changing API key
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
const $openaiBaseUrl = this.$widget.find('.openai-base-url');
|
||||
$openaiBaseUrl.on('change', async () => {
|
||||
await this.updateOption('openaiBaseUrl', $openaiBaseUrl.val() as string);
|
||||
// Display validation warnings after changing URL
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
const $openaiDefaultModel = this.$widget.find('.openai-default-model');
|
||||
$openaiDefaultModel.on('change', async () => {
|
||||
await this.updateOption('openaiDefaultModel', $openaiDefaultModel.val() as string);
|
||||
});
|
||||
|
||||
const $openaiEmbeddingModel = this.$widget.find('.openai-embedding-model');
|
||||
$openaiEmbeddingModel.on('change', async () => {
|
||||
await this.updateOption('openaiEmbeddingModel', $openaiEmbeddingModel.val() as string);
|
||||
});
|
||||
|
||||
// Anthropic options
|
||||
const $anthropicApiKey = this.$widget.find('.anthropic-api-key');
|
||||
$anthropicApiKey.on('change', async () => {
|
||||
await this.updateOption('anthropicApiKey', $anthropicApiKey.val() as string);
|
||||
// Display validation warnings after changing API key
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
const $anthropicDefaultModel = this.$widget.find('.anthropic-default-model');
|
||||
$anthropicDefaultModel.on('change', async () => {
|
||||
await this.updateOption('anthropicDefaultModel', $anthropicDefaultModel.val() as string);
|
||||
});
|
||||
|
||||
const $anthropicBaseUrl = this.$widget.find('.anthropic-base-url');
|
||||
$anthropicBaseUrl.on('change', async () => {
|
||||
await this.updateOption('anthropicBaseUrl', $anthropicBaseUrl.val() as string);
|
||||
});
|
||||
|
||||
const $voyageApiKey = this.$widget.find('.voyage-api-key');
|
||||
$voyageApiKey.on('change', async () => {
|
||||
await this.updateOption('voyageApiKey', $voyageApiKey.val() as string);
|
||||
});
|
||||
|
||||
const $voyageEmbeddingModel = this.$widget.find('.voyage-embedding-model');
|
||||
$voyageEmbeddingModel.on('change', async () => {
|
||||
await this.updateOption('voyageEmbeddingModel', $voyageEmbeddingModel.val() as string);
|
||||
});
|
||||
|
||||
const $ollamaBaseUrl = this.$widget.find('.ollama-base-url');
|
||||
$ollamaBaseUrl.on('change', async () => {
|
||||
await this.updateOption('ollamaBaseUrl', $ollamaBaseUrl.val() as string);
|
||||
});
|
||||
|
||||
const $ollamaDefaultModel = this.$widget.find('.ollama-default-model');
|
||||
$ollamaDefaultModel.on('change', async () => {
|
||||
await this.updateOption('ollamaDefaultModel', $ollamaDefaultModel.val() as string);
|
||||
});
|
||||
|
||||
const $ollamaEmbeddingModel = this.$widget.find('.ollama-embedding-model');
|
||||
$ollamaEmbeddingModel.on('change', async () => {
|
||||
await this.updateOption('ollamaEmbeddingModel', $ollamaEmbeddingModel.val() as string);
|
||||
});
|
||||
|
||||
const $refreshModels = this.$widget.find('.refresh-models');
|
||||
$refreshModels.on('click', async () => {
|
||||
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(true, this.ollamaModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for Ollama tab
|
||||
const $ollamaTab = this.$widget.find('#nav-ollama-tab');
|
||||
$ollamaTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.ollamaModelsRefreshed = await this.providerService?.refreshOllamaModels(false, this.ollamaModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// OpenAI models refresh button
|
||||
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
|
||||
$refreshOpenAIModels.on('click', async () => {
|
||||
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(true, this.openaiModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for OpenAI tab
|
||||
const $openaiTab = this.$widget.find('#nav-openai-tab');
|
||||
$openaiTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.openaiModelsRefreshed = await this.providerService?.refreshOpenAIModels(false, this.openaiModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Anthropic models refresh button
|
||||
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
|
||||
$refreshAnthropicModels.on('click', async () => {
|
||||
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(true, this.anthropicModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Add tab change handler for Anthropic tab
|
||||
const $anthropicTab = this.$widget.find('#nav-anthropic-tab');
|
||||
$anthropicTab.on('shown.bs.tab', async () => {
|
||||
// Only refresh the models if we haven't done it before
|
||||
this.anthropicModelsRefreshed = await this.providerService?.refreshAnthropicModels(false, this.anthropicModelsRefreshed) || false;
|
||||
});
|
||||
|
||||
// Embedding options event handlers
|
||||
const $embeddingAutoUpdateEnabled = this.$widget.find('.embedding-auto-update-enabled');
|
||||
$embeddingAutoUpdateEnabled.on('change', async () => {
|
||||
await this.updateOption('embeddingAutoUpdateEnabled', $embeddingAutoUpdateEnabled.prop('checked') ? "true" : "false");
|
||||
});
|
||||
|
||||
const $enableAutomaticIndexing = this.$widget.find('.enable-automatic-indexing');
|
||||
$enableAutomaticIndexing.on('change', async () => {
|
||||
await this.updateOption('enableAutomaticIndexing', $enableAutomaticIndexing.prop('checked') ? "true" : "false");
|
||||
});
|
||||
|
||||
const $embeddingSimilarityThreshold = this.$widget.find('.embedding-similarity-threshold');
|
||||
$embeddingSimilarityThreshold.on('change', async () => {
|
||||
await this.updateOption('embeddingSimilarityThreshold', $embeddingSimilarityThreshold.val() as string);
|
||||
});
|
||||
|
||||
const $maxNotesPerLlmQuery = this.$widget.find('.max-notes-per-llm-query');
|
||||
$maxNotesPerLlmQuery.on('change', async () => {
|
||||
await this.updateOption('maxNotesPerLlmQuery', $maxNotesPerLlmQuery.val() as string);
|
||||
});
|
||||
|
||||
const $embeddingDefaultProvider = this.$widget.find('.embedding-default-provider');
|
||||
$embeddingDefaultProvider.on('change', async () => {
|
||||
await this.updateOption('embeddingsDefaultProvider', $embeddingDefaultProvider.val() as string);
|
||||
// Display validation warnings after changing default provider
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
const $embeddingDimensionStrategy = this.$widget.find('.embedding-dimension-strategy');
|
||||
$embeddingDimensionStrategy.on('change', async () => {
|
||||
await this.updateOption('embeddingDimensionStrategy', $embeddingDimensionStrategy.val() as string);
|
||||
});
|
||||
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
$embeddingProviderPrecedence.on('change', async () => {
|
||||
await this.updateOption('embeddingProviderPrecedence', $embeddingProviderPrecedence.val() as string);
|
||||
// Display validation warnings after changing precedence list
|
||||
await this.displayValidationWarnings();
|
||||
});
|
||||
|
||||
// Set up sortable behavior for the embedding provider precedence list
|
||||
this.setupEmbeddingProviderSortable();
|
||||
this.setupAiProviderSortable();
|
||||
|
||||
// Embedding stats refresh button
|
||||
const $refreshStats = this.$widget.find('.embedding-refresh-stats');
|
||||
$refreshStats.on('click', async () => {
|
||||
await this.refreshEmbeddingStats();
|
||||
await this.fetchFailedEmbeddingNotes();
|
||||
});
|
||||
|
||||
// Rebuild index button
|
||||
const $rebuildIndex = this.$widget.find('.rebuild-embeddings-index');
|
||||
$rebuildIndex.on('click', async () => {
|
||||
try {
|
||||
await server.post('embeddings/rebuild');
|
||||
toastService.showMessage(t("ai_llm.rebuild_index_started"));
|
||||
|
||||
// Start progress polling
|
||||
this.pollIndexRebuildProgress();
|
||||
} catch (e) {
|
||||
console.error('Error starting index rebuild:', e);
|
||||
toastService.showError(t("ai_llm.rebuild_index_error"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display warnings for validation issues with providers
|
||||
*/
|
||||
async displayValidationWarnings() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $warningDiv = this.$widget.find('.provider-validation-warning');
|
||||
|
||||
// Check if AI is enabled
|
||||
const aiEnabled = this.$widget.find('.ai-enabled').prop('checked');
|
||||
if (!aiEnabled) {
|
||||
$warningDiv.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get provider precedence
|
||||
const providerPrecedence = (this.$widget.find('.ai-provider-precedence').val() as string || '').split(',');
|
||||
|
||||
// Check for OpenAI configuration if it's in the precedence list
|
||||
const openaiWarnings = [];
|
||||
if (providerPrecedence.includes('openai')) {
|
||||
const openaiApiKey = this.$widget.find('.openai-api-key').val();
|
||||
if (!openaiApiKey) {
|
||||
openaiWarnings.push(t("ai_llm.warning_openai_missing_api_key"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Anthropic configuration if it's in the precedence list
|
||||
const anthropicWarnings = [];
|
||||
if (providerPrecedence.includes('anthropic')) {
|
||||
const anthropicApiKey = this.$widget.find('.anthropic-api-key').val();
|
||||
if (!anthropicApiKey) {
|
||||
anthropicWarnings.push(t("ai_llm.warning_anthropic_missing_api_key"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Voyage configuration if it's in the precedence list
|
||||
const voyageWarnings = [];
|
||||
if (providerPrecedence.includes('voyage')) {
|
||||
const voyageApiKey = this.$widget.find('.voyage-api-key').val();
|
||||
if (!voyageApiKey) {
|
||||
voyageWarnings.push(t("ai_llm.warning_voyage_missing_api_key"));
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Ollama configuration if it's in the precedence list
|
||||
const ollamaWarnings = [];
|
||||
if (providerPrecedence.includes('ollama')) {
|
||||
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val();
|
||||
if (!ollamaBaseUrl) {
|
||||
ollamaWarnings.push(t("ai_llm.warning_ollama_missing_url"));
|
||||
}
|
||||
}
|
||||
|
||||
// Similar checks for embeddings
|
||||
const embeddingWarnings = [];
|
||||
const embeddingsEnabled = this.$widget.find('.enable-automatic-indexing').prop('checked');
|
||||
|
||||
if (embeddingsEnabled) {
|
||||
const embeddingProviderPrecedence = (this.$widget.find('.embedding-provider-precedence').val() as string || '').split(',');
|
||||
|
||||
if (embeddingProviderPrecedence.includes('openai') && !this.$widget.find('.openai-api-key').val()) {
|
||||
embeddingWarnings.push(t("ai_llm.warning_openai_embedding_missing_api_key"));
|
||||
}
|
||||
|
||||
if (embeddingProviderPrecedence.includes('voyage') && !this.$widget.find('.voyage-api-key').val()) {
|
||||
embeddingWarnings.push(t("ai_llm.warning_voyage_embedding_missing_api_key"));
|
||||
}
|
||||
|
||||
if (embeddingProviderPrecedence.includes('ollama') && !this.$widget.find('.ollama-base-url').val()) {
|
||||
embeddingWarnings.push(t("ai_llm.warning_ollama_embedding_missing_url"));
|
||||
}
|
||||
}
|
||||
|
||||
// Combine all warnings
|
||||
const allWarnings = [
|
||||
...openaiWarnings,
|
||||
...anthropicWarnings,
|
||||
...voyageWarnings,
|
||||
...ollamaWarnings,
|
||||
...embeddingWarnings
|
||||
];
|
||||
|
||||
// Show or hide warnings
|
||||
if (allWarnings.length > 0) {
|
||||
const warningHtml = '<strong>' + t("ai_llm.configuration_warnings") + '</strong><ul>' +
|
||||
allWarnings.map(warning => `<li>${warning}</li>`).join('') + '</ul>';
|
||||
$warningDiv.html(warningHtml).show();
|
||||
} else {
|
||||
$warningDiv.hide();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll for index rebuild progress
|
||||
*/
|
||||
pollIndexRebuildProgress() {
|
||||
if (this.indexRebuildRefreshInterval) {
|
||||
clearInterval(this.indexRebuildRefreshInterval);
|
||||
}
|
||||
|
||||
// Set up polling interval for index rebuild progress
|
||||
this.indexRebuildRefreshInterval = setInterval(async () => {
|
||||
await this.refreshEmbeddingStats();
|
||||
}, this.STATS_REFRESH_INTERVAL);
|
||||
|
||||
// Stop polling after 5 minutes to avoid indefinite polling
|
||||
setTimeout(() => {
|
||||
if (this.indexRebuildRefreshInterval) {
|
||||
clearInterval(this.indexRebuildRefreshInterval);
|
||||
this.indexRebuildRefreshInterval = null;
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh embedding statistics
|
||||
*/
|
||||
async refreshEmbeddingStats() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
try {
|
||||
const response = await server.get<EmbeddingStats>('embeddings/stats');
|
||||
|
||||
if (response && response.success) {
|
||||
const stats = response.stats;
|
||||
|
||||
// Update stats display
|
||||
this.$widget.find('.embedding-processed-notes').text(stats.embeddedNotesCount);
|
||||
this.$widget.find('.embedding-total-notes').text(stats.totalNotesCount);
|
||||
this.$widget.find('.embedding-queued-notes').text(stats.queuedNotesCount);
|
||||
this.$widget.find('.embedding-failed-notes').text(stats.failedNotesCount);
|
||||
|
||||
if (stats.lastProcessedDate) {
|
||||
const date = new Date(stats.lastProcessedDate);
|
||||
this.$widget.find('.embedding-last-processed').text(date.toLocaleString());
|
||||
} else {
|
||||
this.$widget.find('.embedding-last-processed').text('-');
|
||||
}
|
||||
|
||||
// Update progress bar
|
||||
const $progressBar = this.$widget.find('.embedding-progress');
|
||||
const progressPercent = stats.percentComplete;
|
||||
$progressBar.css('width', `${progressPercent}%`);
|
||||
$progressBar.attr('aria-valuenow', progressPercent.toString());
|
||||
$progressBar.text(`${progressPercent}%`);
|
||||
|
||||
// Update status text
|
||||
let statusText;
|
||||
if (stats.queuedNotesCount > 0) {
|
||||
statusText = t("ai_llm.processing");
|
||||
} else if (stats.embeddedNotesCount === 0) {
|
||||
statusText = t("ai_llm.not_started");
|
||||
} else if (stats.embeddedNotesCount === stats.totalNotesCount) {
|
||||
statusText = t("ai_llm.complete");
|
||||
|
||||
// Clear polling interval if processing is complete
|
||||
if (this.indexRebuildRefreshInterval) {
|
||||
clearInterval(this.indexRebuildRefreshInterval);
|
||||
this.indexRebuildRefreshInterval = null;
|
||||
}
|
||||
} else {
|
||||
statusText = t("ai_llm.partial");
|
||||
}
|
||||
|
||||
this.$widget.find('.embedding-status-text').text(statusText);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching embedding stats:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch failed embedding notes
|
||||
*/
|
||||
async fetchFailedEmbeddingNotes() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
try {
|
||||
const response = await server.get<FailedEmbeddingNotes>('embeddings/failed-notes');
|
||||
|
||||
if (response && response.success) {
|
||||
const failedNotes = response.failedNotes || [];
|
||||
const $failedNotesList = this.$widget.find('.embedding-failed-notes-list');
|
||||
|
||||
if (failedNotes.length === 0) {
|
||||
$failedNotesList.html(`<div class="alert alert-info">${t("ai_llm.no_failed_embeddings")}</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a table with failed notes
|
||||
let html = `
|
||||
<table class="table table-sm table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t("ai_llm.note_title")}</th>
|
||||
<th>${t("ai_llm.error")}</th>
|
||||
<th>${t("ai_llm.last_attempt")}</th>
|
||||
<th>${t("ai_llm.actions")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
for (const note of failedNotes) {
|
||||
const date = new Date(note.lastAttempt);
|
||||
const isPermanent = note.isPermanent;
|
||||
const noteTitle = note.title || note.noteId;
|
||||
|
||||
html += `
|
||||
<tr data-note-id="${note.noteId}">
|
||||
<td><a href="#" class="open-note">${noteTitle}</a></td>
|
||||
<td>${note.error}</td>
|
||||
<td>${date.toLocaleString()}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary retry-embedding" ${isPermanent ? 'disabled' : ''}>
|
||||
${t("ai_llm.retry")}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
html += `
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
$failedNotesList.html(html);
|
||||
|
||||
// Add event handlers for retry buttons
|
||||
$failedNotesList.find('.retry-embedding').on('click', async function() {
|
||||
const noteId = $(this).closest('tr').data('note-id');
|
||||
try {
|
||||
await server.post('embeddings/retry', { noteId });
|
||||
toastService.showMessage(t("ai_llm.retry_queued"));
|
||||
// Remove this row or update status
|
||||
$(this).closest('tr').remove();
|
||||
} catch (e) {
|
||||
console.error('Error retrying embedding:', e);
|
||||
toastService.showError(t("ai_llm.retry_failed"));
|
||||
}
|
||||
});
|
||||
|
||||
// Add event handlers for open note links
|
||||
$failedNotesList.find('.open-note').on('click', function(e) {
|
||||
e.preventDefault();
|
||||
const noteId = $(this).closest('tr').data('note-id');
|
||||
window.open(`#${noteId}`, '_blank');
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching failed embedding notes:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to get display name for providers
|
||||
*/
|
||||
getProviderDisplayName(provider: string): string {
|
||||
switch(provider) {
|
||||
case 'openai': return 'OpenAI';
|
||||
case 'anthropic': return 'Anthropic';
|
||||
case 'ollama': return 'Ollama';
|
||||
case 'voyage': return 'Voyage';
|
||||
case 'local': return 'Local';
|
||||
default: return provider.charAt(0).toUpperCase() + provider.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sortable behavior for embedding provider precedence
|
||||
*/
|
||||
setupEmbeddingProviderSortable() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
const $sortableList = this.$widget.find('.embedding-provider-sortable');
|
||||
const $items = $sortableList.find('li');
|
||||
|
||||
// Make list items draggable
|
||||
$items.each((index, item) => this.setupEmbeddingProviderItemDragHandlers($(item)));
|
||||
|
||||
// Setup the remove buttons
|
||||
this.setupEmbeddingProviderRemoveHandlers();
|
||||
|
||||
// Setup disabled providers list restore handlers
|
||||
this.$widget.find('.embedding-provider-disabled li').each((index, item) => {
|
||||
this.setupEmbeddingProviderRestoreHandler($(item));
|
||||
});
|
||||
|
||||
// Initialize the order based on saved value
|
||||
this.initializeEmbeddingProviderOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup sortable behavior for AI provider precedence
|
||||
*/
|
||||
setupAiProviderSortable() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $aiProviderPrecedence = this.$widget.find('.ai-provider-precedence');
|
||||
const $sortableList = this.$widget.find('.provider-sortable');
|
||||
const $items = $sortableList.find('li');
|
||||
|
||||
// Make list items draggable
|
||||
$items.each((index, item) => this.setupAiItemDragHandlers($(item)));
|
||||
|
||||
// Setup the remove buttons
|
||||
this.setupAiProviderRemoveHandlers();
|
||||
|
||||
// Setup disabled providers list restore handlers
|
||||
this.$widget.find('.provider-disabled li').each((index, item) => {
|
||||
this.setupAiProviderRestoreHandler($(item));
|
||||
});
|
||||
|
||||
// Initialize the order based on saved value
|
||||
this.initializeAiProviderOrder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag handlers for an embedding provider list item
|
||||
*/
|
||||
setupEmbeddingProviderItemDragHandlers($item: JQuery) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const self = this;
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
const $embeddingSortableList = this.$widget.find('.embedding-provider-sortable');
|
||||
|
||||
// Setup dragstart handler
|
||||
$item.on('dragstart', function(e: JQuery.DragStartEvent) {
|
||||
$(this).addClass('dragging');
|
||||
e.originalEvent?.dataTransfer?.setData('text/plain', '');
|
||||
});
|
||||
|
||||
// Setup dragend handler
|
||||
$item.on('dragend', function() {
|
||||
$(this).removeClass('dragging');
|
||||
|
||||
// Update the hidden input value
|
||||
const providers = $embeddingSortableList.find('li').map(function() {
|
||||
return $(this).data('provider');
|
||||
}).get().join(',');
|
||||
|
||||
// Only update if we have providers or if the current value isn't empty
|
||||
// This prevents setting an empty string when all providers are removed
|
||||
if (providers || $embeddingProviderPrecedence.val()) {
|
||||
$embeddingProviderPrecedence.val(providers);
|
||||
$embeddingProviderPrecedence.trigger('change');
|
||||
}
|
||||
});
|
||||
|
||||
// Additional drag event handlers ...
|
||||
|
||||
// All other drag event handlers would be implemented here
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for embedding provider remove buttons
|
||||
*/
|
||||
setupEmbeddingProviderRemoveHandlers() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const self = this;
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
const $embeddingSortableList = this.$widget.find('.embedding-provider-sortable');
|
||||
|
||||
// Remove any existing handlers to prevent duplicates
|
||||
this.$widget.find('.remove-provider').off('click');
|
||||
|
||||
// Add handlers
|
||||
this.$widget.find('.remove-provider').on('click', function() {
|
||||
const $item = $(this).closest('li');
|
||||
const provider = $item.data('provider');
|
||||
const providerName = self.getProviderDisplayName(provider);
|
||||
|
||||
// Create a new item for the disabled list
|
||||
const $disabledItem = $(`
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="${provider}">
|
||||
<strong class="flex-grow-1">${providerName}</strong>
|
||||
<button class="icon-action restore-provider" title="${t("ai_llm.restore_provider")}">
|
||||
<span class="bx bx-plus"></span>
|
||||
</button>
|
||||
</li>
|
||||
`);
|
||||
|
||||
// Move to disabled list
|
||||
self.$widget?.find('.embedding-provider-disabled').append($disabledItem);
|
||||
self.setupEmbeddingProviderRestoreHandler($disabledItem);
|
||||
$item.remove();
|
||||
|
||||
// Update the precedence value
|
||||
const providers = $embeddingSortableList.find('li').map(function() {
|
||||
return $(this).data('provider');
|
||||
}).get().join(',');
|
||||
$embeddingProviderPrecedence.val(providers);
|
||||
$embeddingProviderPrecedence.trigger('change');
|
||||
|
||||
// Show disabled providers container
|
||||
self.$widget?.find('.disabled-providers-container').show();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handler for embedding provider restore button
|
||||
*/
|
||||
setupEmbeddingProviderRestoreHandler($item: JQuery) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const self = this;
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
const $embeddingSortableList = this.$widget.find('.embedding-provider-sortable');
|
||||
|
||||
// Remove any existing handlers to prevent duplicates
|
||||
$item.find('.restore-provider').off('click');
|
||||
|
||||
// Add handlers
|
||||
$item.find('.restore-provider').on('click', function() {
|
||||
const $disabledItem = $(this).closest('li');
|
||||
const provider = $disabledItem.data('provider');
|
||||
const providerName = self.getProviderDisplayName(provider);
|
||||
|
||||
// Create a new item for the active list
|
||||
const $activeItem = $(`
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="${provider}" draggable="true">
|
||||
<span class="drag-handle bx bx-dots-vertical-rounded me-2"></span>
|
||||
<strong class="flex-grow-1">${providerName}</strong>
|
||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||
<span class="bx bx-x"></span>
|
||||
</button>
|
||||
</li>
|
||||
`);
|
||||
|
||||
// Move to active list
|
||||
$embeddingSortableList.append($activeItem);
|
||||
self.setupEmbeddingProviderItemDragHandlers($activeItem);
|
||||
self.setupEmbeddingProviderRemoveHandlers();
|
||||
$disabledItem.remove();
|
||||
|
||||
// Update the precedence value
|
||||
const providers = $embeddingSortableList.find('li').map(function() {
|
||||
return $(this).data('provider');
|
||||
}).get().join(',');
|
||||
$embeddingProviderPrecedence.val(providers);
|
||||
$embeddingProviderPrecedence.trigger('change');
|
||||
|
||||
// Hide disabled providers container if it's now empty
|
||||
if (self.$widget?.find('.embedding-provider-disabled li').length === 0) {
|
||||
self.$widget?.find('.disabled-providers-container').hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the embedding provider precedence order based on saved values
|
||||
*/
|
||||
initializeEmbeddingProviderOrder() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $embeddingProviderPrecedence = this.$widget.find('.embedding-provider-precedence');
|
||||
const $sortableList = this.$widget.find('.embedding-provider-sortable');
|
||||
|
||||
// Get the current value
|
||||
const savedValue = $embeddingProviderPrecedence.val() as string;
|
||||
// If no saved value, don't proceed with initialization to avoid triggering the "empty" change
|
||||
if (!savedValue) return;
|
||||
|
||||
// Get all available providers
|
||||
const allProviders = ['openai', 'voyage', 'ollama', 'local'];
|
||||
const savedProviders = savedValue.split(',');
|
||||
|
||||
// Clear all items from the disabled list first to avoid duplicates
|
||||
this.$widget.find('.embedding-provider-disabled').empty();
|
||||
|
||||
// Find disabled providers (providers in allProviders but not in savedProviders)
|
||||
const disabledProviders = allProviders.filter(p => !savedProviders.includes(p));
|
||||
|
||||
// Move saved providers to the end in the correct order
|
||||
savedProviders.forEach(provider => {
|
||||
const $item = $sortableList.find(`li[data-provider="${provider}"]`);
|
||||
if ($item.length) {
|
||||
$sortableList.append($item); // Move to the end in the correct order
|
||||
}
|
||||
});
|
||||
|
||||
// Setup remove click handlers first to ensure they work when simulating clicks
|
||||
this.setupEmbeddingProviderRemoveHandlers();
|
||||
|
||||
// Move disabled providers to the disabled list
|
||||
disabledProviders.forEach(provider => {
|
||||
const $item = $sortableList.find(`li[data-provider="${provider}"]`);
|
||||
if ($item.length) {
|
||||
// Simulate clicking the remove button to move it to the disabled list
|
||||
$item.find('.remove-provider').trigger('click');
|
||||
} else {
|
||||
// If it's not in the active list already, manually create it in the disabled list
|
||||
const providerName = this.getProviderDisplayName(provider);
|
||||
const $disabledItem = $(`
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="${provider}">
|
||||
<strong class="flex-grow-1">${providerName}</strong>
|
||||
<button class="icon-action restore-provider" title="${t("ai_llm.restore_provider")}">
|
||||
<span class="bx bx-plus"></span>
|
||||
</button>
|
||||
</li>
|
||||
`);
|
||||
this.$widget.find('.embedding-provider-disabled').append($disabledItem);
|
||||
|
||||
// Add restore button handler
|
||||
this.setupEmbeddingProviderRestoreHandler($disabledItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide the disabled providers container
|
||||
const $disabledContainer = this.$widget.find('.disabled-providers-container');
|
||||
const hasDisabledProviders = this.$widget.find('.embedding-provider-disabled li').length > 0;
|
||||
$disabledContainer.toggle(hasDisabledProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup drag handlers for an AI provider list item
|
||||
*/
|
||||
setupAiItemDragHandlers($item: JQuery) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const self = this;
|
||||
const $aiProviderPrecedence = this.$widget.find('.ai-provider-precedence');
|
||||
const $aiSortableList = this.$widget.find('.provider-sortable');
|
||||
|
||||
// Setup dragstart handler
|
||||
$item.on('dragstart', function(e: JQuery.DragStartEvent) {
|
||||
$(this).addClass('dragging');
|
||||
e.originalEvent?.dataTransfer?.setData('text/plain', '');
|
||||
});
|
||||
|
||||
// Setup dragend handler
|
||||
$item.on('dragend', function() {
|
||||
$(this).removeClass('dragging');
|
||||
|
||||
// Update the hidden input value
|
||||
const providers = $aiSortableList.find('li').map(function() {
|
||||
return $(this).data('provider');
|
||||
}).get().join(',');
|
||||
|
||||
$aiProviderPrecedence.val(providers);
|
||||
$aiProviderPrecedence.trigger('change');
|
||||
});
|
||||
|
||||
// Additional drag event handlers would go here...
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the AI provider precedence order based on saved values
|
||||
*/
|
||||
initializeAiProviderOrder() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
const $aiProviderPrecedence = this.$widget.find('.ai-provider-precedence');
|
||||
const $aiSortableList = this.$widget.find('.provider-sortable');
|
||||
|
||||
// Get the current value
|
||||
const savedValue = $aiProviderPrecedence.val() as string;
|
||||
if (!savedValue) return;
|
||||
|
||||
// Get all available providers
|
||||
const allProviders = ['openai', 'anthropic', 'ollama', 'voyage'];
|
||||
const savedProviders = savedValue.split(',');
|
||||
|
||||
// Clear all items from the disabled list first to avoid duplicates
|
||||
this.$widget.find('.provider-disabled').empty();
|
||||
|
||||
// Find disabled providers (providers in allProviders but not in savedProviders)
|
||||
const disabledProviders = allProviders.filter(p => !savedProviders.includes(p));
|
||||
|
||||
// Move saved providers to the end in the correct order
|
||||
savedProviders.forEach(provider => {
|
||||
const $item = $aiSortableList.find(`li[data-provider="${provider}"]`);
|
||||
if ($item.length) {
|
||||
$aiSortableList.append($item); // Move to the end in the correct order
|
||||
}
|
||||
});
|
||||
|
||||
// Setup remove click handlers first to ensure they work when simulating clicks
|
||||
this.setupAiProviderRemoveHandlers();
|
||||
|
||||
// Move disabled providers to the disabled list
|
||||
disabledProviders.forEach(provider => {
|
||||
const $item = $aiSortableList.find(`li[data-provider="${provider}"]`);
|
||||
if ($item.length) {
|
||||
// Simulate clicking the remove button to move it to the disabled list
|
||||
$item.find('.remove-ai-provider').trigger('click');
|
||||
} else {
|
||||
// If it's not in the active list already, manually create it in the disabled list
|
||||
const providerName = this.getProviderDisplayName(provider);
|
||||
const $disabledItem = $(`
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="${provider}">
|
||||
<strong class="flex-grow-1">${providerName}</strong>
|
||||
<button class="icon-action restore-ai-provider" title="${t("ai_llm.restore_provider")}">
|
||||
<span class="bx bx-plus"></span>
|
||||
</button>
|
||||
</li>
|
||||
`);
|
||||
this.$widget.find('.provider-disabled').append($disabledItem);
|
||||
|
||||
// Add restore button handler
|
||||
this.setupAiProviderRestoreHandler($disabledItem);
|
||||
}
|
||||
});
|
||||
|
||||
// Show/hide the disabled providers container
|
||||
const $disabledContainer = this.$widget.find('.disabled-ai-providers-container');
|
||||
const hasDisabledProviders = this.$widget.find('.provider-disabled li').length > 0;
|
||||
$disabledContainer.toggle(hasDisabledProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handlers for AI provider remove buttons
|
||||
*/
|
||||
setupAiProviderRemoveHandlers() {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// Implementation would go here...
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event handler for AI provider restore button
|
||||
*/
|
||||
setupAiProviderRestoreHandler($item: JQuery) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// Implementation would go here...
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the options have been loaded from the server
|
||||
*/
|
||||
optionsLoaded(options: OptionMap) {
|
||||
if (!this.$widget) return;
|
||||
|
||||
// AI Options
|
||||
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');
|
||||
|
||||
// OpenAI Section
|
||||
this.$widget.find('.openai-api-key').val(options.openaiApiKey || '');
|
||||
this.$widget.find('.openai-base-url').val(options.openaiBaseUrl || 'https://api.openai.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');
|
||||
|
||||
// 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');
|
||||
|
||||
// Voyage Section
|
||||
this.$widget.find('.voyage-api-key').val(options.voyageApiKey || '');
|
||||
this.$widget.find('.voyage-embedding-model').val(options.voyageEmbeddingModel || 'voyage-2');
|
||||
|
||||
// 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');
|
||||
|
||||
// Embedding Options
|
||||
this.$widget.find('.embedding-provider-precedence').val(options.embeddingProviderPrecedence || 'openai,voyage,ollama,local');
|
||||
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');
|
||||
this.$widget.find('.max-notes-per-llm-query').val(options.maxNotesPerLlmQuery || '3');
|
||||
this.$widget.find('.embedding-dimension-strategy').val(options.embeddingDimensionStrategy || 'auto');
|
||||
|
||||
// Initialize sortable lists
|
||||
this.initializeEmbeddingProviderOrder();
|
||||
this.initializeAiProviderOrder();
|
||||
|
||||
// Display validation warnings
|
||||
this.displayValidationWarnings();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
// Clear intervals
|
||||
if (this.statsRefreshInterval) {
|
||||
clearInterval(this.statsRefreshInterval);
|
||||
this.statsRefreshInterval = null;
|
||||
}
|
||||
|
||||
if (this.indexRebuildRefreshInterval) {
|
||||
clearInterval(this.indexRebuildRefreshInterval);
|
||||
this.indexRebuildRefreshInterval = null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
import AiSettingsWidget from './ai_settings_widget.js';
|
||||
export default AiSettingsWidget;
|
@ -0,0 +1,69 @@
|
||||
// Interface for the Ollama model response
|
||||
export interface OllamaModelResponse {
|
||||
success: boolean;
|
||||
models: Array<{
|
||||
name: string;
|
||||
model: string;
|
||||
details?: {
|
||||
family?: string;
|
||||
parameter_size?: string;
|
||||
}
|
||||
}>;
|
||||
}
|
||||
|
||||
// Interface for embedding statistics
|
||||
export interface EmbeddingStats {
|
||||
success: boolean;
|
||||
stats: {
|
||||
totalNotesCount: number;
|
||||
embeddedNotesCount: number;
|
||||
queuedNotesCount: number;
|
||||
failedNotesCount: number;
|
||||
lastProcessedDate: string | null;
|
||||
percentComplete: number;
|
||||
}
|
||||
}
|
||||
|
||||
// Interface for failed embedding notes
|
||||
export interface FailedEmbeddingNotes {
|
||||
success: boolean;
|
||||
failedNotes: Array<{
|
||||
noteId: string;
|
||||
title?: string;
|
||||
operation: string;
|
||||
attempts: number;
|
||||
lastAttempt: string;
|
||||
error: string;
|
||||
failureType: string;
|
||||
chunks: number;
|
||||
isPermanent: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface OpenAIModelResponse {
|
||||
success: boolean;
|
||||
chatModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
embeddingModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface AnthropicModelResponse {
|
||||
success: boolean;
|
||||
chatModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
embeddingModels: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
@ -0,0 +1,305 @@
|
||||
import server from "../../../../services/server.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import { OpenAIModelResponse, AnthropicModelResponse, OllamaModelResponse } from "./interfaces.js";
|
||||
|
||||
export class ProviderService {
|
||||
constructor(private $widget: JQuery<HTMLElement>) {}
|
||||
|
||||
/**
|
||||
* Refreshes the list of OpenAI models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param openaiModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshOpenAIModels(showLoading: boolean, openaiModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshOpenAIModels = this.$widget.find('.refresh-openai-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (openaiModelsRefreshed && !showLoading) {
|
||||
return openaiModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshOpenAIModels.prop('disabled', true);
|
||||
$refreshOpenAIModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
|
||||
}
|
||||
|
||||
try {
|
||||
const openaiBaseUrl = this.$widget.find('.openai-base-url').val() as string;
|
||||
const response = await server.post<OpenAIModelResponse>('openai/list-models', { baseUrl: openaiBaseUrl });
|
||||
|
||||
if (response && response.success) {
|
||||
// Update the chat models dropdown
|
||||
if (response.chatModels?.length > 0) {
|
||||
const $chatModelSelect = this.$widget.find('.openai-default-model');
|
||||
const currentChatValue = $chatModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$chatModelSelect.empty();
|
||||
|
||||
// Sort models by name
|
||||
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add models to the dropdown
|
||||
sortedChatModels.forEach(model => {
|
||||
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
if (currentChatValue) {
|
||||
$chatModelSelect.val(currentChatValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$chatModelSelect.val()) {
|
||||
$chatModelSelect.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the embedding models dropdown
|
||||
if (response.embeddingModels?.length > 0) {
|
||||
const $embedModelSelect = this.$widget.find('.openai-embedding-model');
|
||||
const currentEmbedValue = $embedModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$embedModelSelect.empty();
|
||||
|
||||
// Sort models by name
|
||||
const sortedEmbedModels = [...response.embeddingModels].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add models to the dropdown
|
||||
sortedEmbedModels.forEach(model => {
|
||||
$embedModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
if (currentEmbedValue) {
|
||||
$embedModelSelect.val(currentEmbedValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$embedModelSelect.val()) {
|
||||
$embedModelSelect.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
// Show success message
|
||||
const totalModels = (response.chatModels?.length || 0) + (response.embeddingModels?.length || 0);
|
||||
toastService.showMessage(`${totalModels} OpenAI models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No OpenAI models found. Please check your API key and settings.`);
|
||||
}
|
||||
|
||||
return openaiModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching OpenAI models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching OpenAI models: ${e}`);
|
||||
}
|
||||
return openaiModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshOpenAIModels.prop('disabled', false);
|
||||
$refreshOpenAIModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of Anthropic models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param anthropicModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshAnthropicModels(showLoading: boolean, anthropicModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshAnthropicModels = this.$widget.find('.refresh-anthropic-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (anthropicModelsRefreshed && !showLoading) {
|
||||
return anthropicModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshAnthropicModels.prop('disabled', true);
|
||||
$refreshAnthropicModels.html(`<i class="spinner-border spinner-border-sm"></i>`);
|
||||
}
|
||||
|
||||
try {
|
||||
const anthropicBaseUrl = this.$widget.find('.anthropic-base-url').val() as string;
|
||||
const response = await server.post<AnthropicModelResponse>('anthropic/list-models', { baseUrl: anthropicBaseUrl });
|
||||
|
||||
if (response && response.success) {
|
||||
// Update the chat models dropdown
|
||||
if (response.chatModels?.length > 0) {
|
||||
const $chatModelSelect = this.$widget.find('.anthropic-default-model');
|
||||
const currentChatValue = $chatModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$chatModelSelect.empty();
|
||||
|
||||
// Sort models by name
|
||||
const sortedChatModels = [...response.chatModels].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add models to the dropdown
|
||||
sortedChatModels.forEach(model => {
|
||||
$chatModelSelect.append(`<option value="${model.id}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
if (currentChatValue) {
|
||||
$chatModelSelect.val(currentChatValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$chatModelSelect.val()) {
|
||||
$chatModelSelect.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle embedding models if they exist
|
||||
if (response.embeddingModels?.length > 0 && showLoading) {
|
||||
toastService.showMessage(`Found ${response.embeddingModels.length} Anthropic embedding models.`);
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
// Show success message
|
||||
const totalModels = (response.chatModels?.length || 0) + (response.embeddingModels?.length || 0);
|
||||
toastService.showMessage(`${totalModels} Anthropic models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No Anthropic models found. Please check your API key and settings.`);
|
||||
}
|
||||
|
||||
return anthropicModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching Anthropic models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching Anthropic models: ${e}`);
|
||||
}
|
||||
return anthropicModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshAnthropicModels.prop('disabled', false);
|
||||
$refreshAnthropicModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the list of Ollama models
|
||||
* @param showLoading Whether to show loading indicators and toasts
|
||||
* @param ollamaModelsRefreshed Reference to track if models have been refreshed
|
||||
* @returns Promise that resolves when the refresh is complete
|
||||
*/
|
||||
async refreshOllamaModels(showLoading: boolean, ollamaModelsRefreshed: boolean): Promise<boolean> {
|
||||
if (!this.$widget) return false;
|
||||
|
||||
const $refreshModels = this.$widget.find('.refresh-models');
|
||||
|
||||
// If we've already refreshed and we're not forcing a refresh, don't do it again
|
||||
if (ollamaModelsRefreshed && !showLoading) {
|
||||
return ollamaModelsRefreshed;
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
$refreshModels.prop('disabled', true);
|
||||
$refreshModels.text(t("ai_llm.refreshing_models"));
|
||||
}
|
||||
|
||||
try {
|
||||
const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val() as string;
|
||||
const response = await server.post<OllamaModelResponse>('ollama/list-models', { baseUrl: ollamaBaseUrl });
|
||||
|
||||
if (response && response.success && response.models && response.models.length > 0) {
|
||||
const $embedModelSelect = this.$widget.find('.ollama-embedding-model');
|
||||
const currentValue = $embedModelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$embedModelSelect.empty();
|
||||
|
||||
// Add embedding-specific models first
|
||||
const embeddingModels = response.models.filter(model =>
|
||||
model.name.includes('embed') || model.name.includes('bert'));
|
||||
|
||||
embeddingModels.forEach(model => {
|
||||
$embedModelSelect.append(`<option value="${model.name}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
if (embeddingModels.length > 0) {
|
||||
// Add separator if we have embedding models
|
||||
$embedModelSelect.append(`<option disabled>─────────────</option>`);
|
||||
}
|
||||
|
||||
// Then add general models which can be used for embeddings too
|
||||
const generalModels = response.models.filter(model =>
|
||||
!model.name.includes('embed') && !model.name.includes('bert'));
|
||||
|
||||
generalModels.forEach(model => {
|
||||
$embedModelSelect.append(`<option value="${model.name}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
if (currentValue) {
|
||||
$embedModelSelect.val(currentValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$embedModelSelect.val()) {
|
||||
$embedModelSelect.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Also update the LLM model dropdown
|
||||
const $modelSelect = this.$widget.find('.ollama-default-model');
|
||||
const currentModelValue = $modelSelect.val();
|
||||
|
||||
// Clear existing options
|
||||
$modelSelect.empty();
|
||||
|
||||
// Sort models by name to make them easier to find
|
||||
const sortedModels = [...response.models].sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Add all models to the dropdown
|
||||
sortedModels.forEach(model => {
|
||||
$modelSelect.append(`<option value="${model.name}">${model.name}</option>`);
|
||||
});
|
||||
|
||||
// Try to restore the previously selected value
|
||||
if (currentModelValue) {
|
||||
$modelSelect.val(currentModelValue);
|
||||
// If the value doesn't exist anymore, select the first option
|
||||
if (!$modelSelect.val()) {
|
||||
$modelSelect.prop('selectedIndex', 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (showLoading) {
|
||||
toastService.showMessage(`${response.models.length} Ollama models found.`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else if (showLoading) {
|
||||
toastService.showError(`No Ollama models found. Please check if Ollama is running.`);
|
||||
}
|
||||
|
||||
return ollamaModelsRefreshed;
|
||||
} catch (e) {
|
||||
console.error(`Error fetching Ollama models:`, e);
|
||||
if (showLoading) {
|
||||
toastService.showError(`Error fetching Ollama models: ${e}`);
|
||||
}
|
||||
return ollamaModelsRefreshed;
|
||||
} finally {
|
||||
if (showLoading) {
|
||||
$refreshModels.prop('disabled', false);
|
||||
$refreshModels.html(`<span class="bx bx-refresh"></span>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,326 @@
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
|
||||
export const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("ai_llm.title")}</h4>
|
||||
|
||||
<!-- Add warning alert div -->
|
||||
<div class="provider-validation-warning alert alert-warning" style="display: none;"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="tn-checkbox">
|
||||
<input class="ai-enabled form-check-input" type="checkbox">
|
||||
${t("ai_llm.enable_ai_features")}
|
||||
</label>
|
||||
<div class="form-text">${t("ai_llm.enable_ai_description")}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>${t("ai_llm.embedding_statistics")}</h4>
|
||||
<div class="embedding-stats-container">
|
||||
<div class="embedding-stats">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div><strong>${t("ai_llm.processed_notes")}:</strong> <span class="embedding-processed-notes">-</span></div>
|
||||
<div><strong>${t("ai_llm.total_notes")}:</strong> <span class="embedding-total-notes">-</span></div>
|
||||
<div><strong>${t("ai_llm.progress")}:</strong> <span class="embedding-status-text">-</span></div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div><strong>${t("ai_llm.queued_notes")}:</strong> <span class="embedding-queued-notes">-</span></div>
|
||||
<div><strong>${t("ai_llm.failed_notes")}:</strong> <span class="embedding-failed-notes">-</span></div>
|
||||
<div><strong>${t("ai_llm.last_processed")}:</strong> <span class="embedding-last-processed">-</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress mt-1" style="height: 10px;">
|
||||
<div class="progress-bar embedding-progress" role="progressbar" style="width: 0%;"
|
||||
aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button class="btn btn-sm btn-outline-secondary embedding-refresh-stats">
|
||||
${t("ai_llm.refresh_stats")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<!-- Failed embeddings section -->
|
||||
<h5>${t("ai_llm.failed_notes")}</h4>
|
||||
<div class="form-group mt-4">
|
||||
<div class="embedding-failed-notes-container">
|
||||
<div class="embedding-failed-notes-list">
|
||||
<div class="alert alert-info">${t("ai_llm.no_failed_embeddings")}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-providers-section options-section">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.temperature")}</label>
|
||||
<input class="ai-temperature form-control" type="number" min="0" max="2" step="0.1">
|
||||
<div class="form-text">${t("ai_llm.temperature_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.system_prompt")}</label>
|
||||
<textarea class="ai-system-prompt form-control" rows="3"></textarea>
|
||||
<div class="form-text">${t("ai_llm.system_prompt_description")}</div>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.embedding_dimension_strategy")}</label>
|
||||
<select class="embedding-dimension-strategy form-control">
|
||||
<option value="auto">${t("ai_llm.embedding_dimension_auto")}</option>
|
||||
<option value="fixed-768">${t("ai_llm.embedding_dimension_fixed")} (768)</option>
|
||||
<option value="fixed-1024">${t("ai_llm.embedding_dimension_fixed")} (1024)</option>
|
||||
<option value="fixed-1536">${t("ai_llm.embedding_dimension_fixed")} (1536)</option>
|
||||
</select>
|
||||
<div class="form-text">${t("ai_llm.embedding_dimension_strategy_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.embedding_similarity_threshold")}</label>
|
||||
<input class="embedding-similarity-threshold form-control" type="number" min="0" max="1" step="0.01">
|
||||
<div class="form-text">${t("ai_llm.embedding_similarity_threshold_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("ai_llm.max_notes_per_llm_query")}</label>
|
||||
<input class="max-notes-per-llm-query form-control" type="number" min="1" max="20" step="1">
|
||||
<div class="form-text">${t("ai_llm.max_notes_per_llm_query_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="tn-checkbox">
|
||||
<input class="enable-automatic-indexing form-check-input" type="checkbox">
|
||||
${t("ai_llm.enable_automatic_indexing")}
|
||||
</label>
|
||||
<div class="form-text">${t("ai_llm.enable_automatic_indexing_description")}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-3">
|
||||
<label class="tn-checkbox">
|
||||
<input class="embedding-auto-update-enabled form-check-input" type="checkbox">
|
||||
${t("ai_llm.embedding_auto_update_enabled")}
|
||||
</label>
|
||||
<div class="form-text">${t("ai_llm.embedding_auto_update_enabled_description")}</div>
|
||||
</div>
|
||||
|
||||
<!-- Rebuild index button with counter -->
|
||||
<div class="form-group mt-3">
|
||||
<button class="btn btn-outline-primary rebuild-embeddings-index">
|
||||
${t("ai_llm.rebuild_index")}
|
||||
</button>
|
||||
<div class="form-text">${t("ai_llm.rebuild_index_description")}</div>
|
||||
</div>
|
||||
|
||||
<!-- Provider order container -->
|
||||
<div class="form-group mt-3">
|
||||
<h5>${t("ai_llm.provider_order")}</h5>
|
||||
<div class="form-text">${t("ai_llm.provider_order_description")}</div>
|
||||
|
||||
<div class="provider-order-container">
|
||||
<ul class="embedding-provider-sortable standard-list mt-2">
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="openai" draggable="true">
|
||||
<span class="drag-handle bx bx-dots-vertical-rounded me-2"></span>
|
||||
<strong class="flex-grow-1">OpenAI</strong>
|
||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||
<span class="bx bx-x"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="voyage" draggable="true">
|
||||
<span class="drag-handle bx bx-dots-vertical-rounded me-2"></span>
|
||||
<strong class="flex-grow-1">Voyage</strong>
|
||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||
<span class="bx bx-x"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="ollama" draggable="true">
|
||||
<span class="drag-handle bx bx-dots-vertical-rounded me-2"></span>
|
||||
<strong class="flex-grow-1">Ollama</strong>
|
||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||
<span class="bx bx-x"></span>
|
||||
</button>
|
||||
</li>
|
||||
<li class="standard-list-item d-flex align-items-center" data-provider="local" draggable="true">
|
||||
<span class="drag-handle bx bx-dots-vertical-rounded me-2"></span>
|
||||
<strong class="flex-grow-1">Local</strong>
|
||||
<button class="icon-action remove-provider" title="${t("ai_llm.remove_provider")}">
|
||||
<span class="bx bx-x"></span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Disabled providers container -->
|
||||
<div class="disabled-providers-container" style="display: none;">
|
||||
<h6 class="mt-3">${t("ai_llm.disabled_providers")}</h6>
|
||||
<ul class="embedding-provider-disabled standard-list mt-2">
|
||||
<!-- Disabled providers will be added here -->
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
Loading…
x
Reference in New Issue
Block a user