mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	feat(llm): remove everything to do with embeddings, part 3
This commit is contained in:
		| @@ -44,18 +44,7 @@ interface OptionRow {} | ||||
|  | ||||
| interface NoteReorderingRow {} | ||||
|  | ||||
| interface NoteEmbeddingRow { | ||||
|     embedId: string; | ||||
|     noteId: string; | ||||
|     providerId: string; | ||||
|     modelId: string; | ||||
|     dimension: number; | ||||
|     version: number; | ||||
|     dateCreated: string; | ||||
|     utcDateCreated: string; | ||||
|     dateModified: string; | ||||
|     utcDateModified: string; | ||||
| } | ||||
|  | ||||
|  | ||||
| type EntityRowMappings = { | ||||
|     notes: NoteRow; | ||||
|   | ||||
| @@ -1195,7 +1195,7 @@ | ||||
|     "restore_provider": "Restore provider to search", | ||||
|     "similarity_threshold": "Similarity Threshold", | ||||
|     "similarity_threshold_description": "Minimum similarity score (0-1) for notes to be included in context for LLM queries", | ||||
|     "reprocess_started": "Embedding reprocessing started in the background", | ||||
|  | ||||
|     "reprocess_index": "Rebuild Search Index", | ||||
|     "reprocessing_index": "Rebuilding...", | ||||
|     "reprocess_index_started": "Search index optimization started in the background", | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import type { OpenAIModelResponse, AnthropicModelResponse, OllamaModelResponse } | ||||
|  | ||||
| export class ProviderService { | ||||
|     constructor(private $widget: JQuery<HTMLElement>) { | ||||
|         // Embedding functionality removed | ||||
|         // AI provider settings | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -204,7 +204,7 @@ export class ProviderService { | ||||
|         try { | ||||
|             // Use the general Ollama base URL | ||||
|             const ollamaBaseUrl = this.$widget.find('.ollama-base-url').val() as string; | ||||
|              | ||||
|  | ||||
|             const response = await server.get<OllamaModelResponse>(`llm/providers/ollama/models?baseUrl=${encodeURIComponent(ollamaBaseUrl)}`); | ||||
|  | ||||
|             if (response && response.success && response.models && response.models.length > 0) { | ||||
|   | ||||
| @@ -16,7 +16,7 @@ export const TPL = ` | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <!-- Embedding statistics section removed --> | ||||
| <!-- AI settings template --> | ||||
|  | ||||
| <div class="ai-providers-section options-section"> | ||||
|     <h4>${t("ai_llm.provider_configuration")}</h4> | ||||
|   | ||||
| @@ -48,17 +48,6 @@ interface AnthropicModel { | ||||
|  *                         type: string | ||||
|  *                       type: | ||||
|  *                         type: string | ||||
|  *                 embeddingModels: | ||||
|  *                   type: array | ||||
|  *                   items: | ||||
|  *                     type: object | ||||
|  *                     properties: | ||||
|  *                       id: | ||||
|  *                         type: string | ||||
|  *                       name: | ||||
|  *                         type: string | ||||
|  *                       type: | ||||
|  *                         type: string | ||||
|  *       '500': | ||||
|  *         description: Error listing models | ||||
|  *     security: | ||||
| @@ -90,14 +79,10 @@ async function listModels(req: Request, res: Response) { | ||||
|             type: 'chat' | ||||
|         })); | ||||
|  | ||||
|         // Anthropic doesn't currently have embedding models | ||||
|         const embeddingModels: AnthropicModel[] = []; | ||||
|  | ||||
|         // Return the models list | ||||
|         return { | ||||
|             success: true, | ||||
|             chatModels, | ||||
|             embeddingModels | ||||
|             chatModels | ||||
|         }; | ||||
|     } catch (error: any) { | ||||
|         log.error(`Error listing Anthropic models: ${error.message || 'Unknown error'}`); | ||||
|   | ||||
| @@ -40,17 +40,6 @@ import OpenAI from "openai"; | ||||
|  *                         type: string | ||||
|  *                       type: | ||||
|  *                         type: string | ||||
|  *                 embeddingModels: | ||||
|  *                   type: array | ||||
|  *                   items: | ||||
|  *                     type: object | ||||
|  *                     properties: | ||||
|  *                       id: | ||||
|  *                         type: string | ||||
|  *                       name: | ||||
|  *                         type: string | ||||
|  *                       type: | ||||
|  *                         type: string | ||||
|  *       '500': | ||||
|  *         description: Error listing models | ||||
|  *     security: | ||||
| @@ -82,8 +71,7 @@ async function listModels(req: Request, res: Response) { | ||||
|         // Filter and categorize models | ||||
|         const allModels = response.data || []; | ||||
|  | ||||
|         // Include all models as chat models, without filtering by specific model names | ||||
|         // This allows models from providers like OpenRouter to be displayed | ||||
|         // Include all models as chat models, excluding embedding models | ||||
|         const chatModels = allModels | ||||
|             .filter((model) => | ||||
|                 // Exclude models that are explicitly for embeddings | ||||
| @@ -96,23 +84,10 @@ async function listModels(req: Request, res: Response) { | ||||
|                 type: 'chat' | ||||
|             })); | ||||
|  | ||||
|         const embeddingModels = allModels | ||||
|             .filter((model) => | ||||
|                 // Only include embedding-specific models | ||||
|                 model.id.includes('embedding') || | ||||
|                 model.id.includes('embed') | ||||
|             ) | ||||
|             .map((model) => ({ | ||||
|                 id: model.id, | ||||
|                 name: model.id, | ||||
|                 type: 'embedding' | ||||
|             })); | ||||
|  | ||||
|         // Return the models list | ||||
|         return { | ||||
|             success: true, | ||||
|             chatModels, | ||||
|             embeddingModels | ||||
|             chatModels | ||||
|         }; | ||||
|     } catch (error: any) { | ||||
|         log.error(`Error listing OpenAI models: ${error.message || 'Unknown error'}`); | ||||
|   | ||||
| @@ -92,7 +92,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([ | ||||
|     "showLoginInShareTheme", | ||||
|     "splitEditorOrientation", | ||||
|  | ||||
|     // AI/LLM integration options (embedding options removed) | ||||
|     // AI/LLM integration options | ||||
|     "aiEnabled", | ||||
|     "aiTemperature", | ||||
|     "aiSystemPrompt", | ||||
|   | ||||
| @@ -1,24 +1,17 @@ | ||||
| /** | ||||
|  * Configuration constants for LLM providers | ||||
|  */ | ||||
| export const PROVIDER_CONSTANTS = { | ||||
|     ANTHROPIC: { | ||||
|         API_VERSION: '2023-06-01', | ||||
|         BETA_VERSION: 'messages-2023-12-15', | ||||
|         BASE_URL: 'https://api.anthropic.com', | ||||
|         DEFAULT_MODEL: 'claude-3-haiku-20240307', | ||||
|         // Model mapping for simplified model names to their full versions | ||||
|         MODEL_MAPPING: { | ||||
|             'claude-3.7-sonnet': 'claude-3-7-sonnet-20250219', | ||||
|             'claude-3.5-sonnet': 'claude-3-5-sonnet-20241022', | ||||
|             'claude-3.5-haiku': 'claude-3-5-haiku-20241022', | ||||
|             'claude-3-opus': 'claude-3-opus-20240229', | ||||
|             'claude-3-sonnet': 'claude-3-sonnet-20240229', | ||||
|             'claude-3-haiku': 'claude-3-haiku-20240307', | ||||
|             'claude-2': 'claude-2.1' | ||||
|         }, | ||||
|         // These are the currently available models from Anthropic | ||||
|         DEFAULT_MODEL: 'claude-3-5-sonnet-20241022', | ||||
|         API_VERSION: '2023-06-01', | ||||
|         BETA_VERSION: undefined, | ||||
|         CONTEXT_WINDOW: 200000, | ||||
|         AVAILABLE_MODELS: [ | ||||
|             { | ||||
|                 id: 'claude-3-7-sonnet-20250219', | ||||
|                 name: 'Claude 3.7 Sonnet', | ||||
|                 id: 'claude-3-5-sonnet-20250106', | ||||
|                 name: 'Claude 3.5 Sonnet (New)', | ||||
|                 description: 'Most intelligent model with hybrid reasoning capabilities', | ||||
|                 maxTokens: 8192 | ||||
|             }, | ||||
| @@ -64,12 +57,7 @@ export const PROVIDER_CONSTANTS = { | ||||
|     OPENAI: { | ||||
|         BASE_URL: 'https://api.openai.com/v1', | ||||
|         DEFAULT_MODEL: 'gpt-3.5-turbo', | ||||
|         DEFAULT_EMBEDDING_MODEL: 'text-embedding-ada-002', | ||||
|         CONTEXT_WINDOW: 16000, | ||||
|         EMBEDDING_DIMENSIONS: { | ||||
|             ADA: 1536, | ||||
|             DEFAULT: 1536 | ||||
|         }, | ||||
|         AVAILABLE_MODELS: [ | ||||
|             { | ||||
|                 id: 'gpt-4o', | ||||
| @@ -132,51 +120,6 @@ export const LLM_CONSTANTS = { | ||||
|         DEFAULT: 6000 | ||||
|     }, | ||||
|  | ||||
|     // Embedding dimensions (verify these with your actual models) | ||||
|     EMBEDDING_DIMENSIONS: { | ||||
|         OLLAMA: { | ||||
|             DEFAULT: 384, | ||||
|             NOMIC: 768, | ||||
|             MISTRAL: 1024 | ||||
|         }, | ||||
|         OPENAI: { | ||||
|             ADA: 1536, | ||||
|             DEFAULT: 1536 | ||||
|         }, | ||||
|         ANTHROPIC: { | ||||
|             CLAUDE: 1024, | ||||
|             DEFAULT: 1024 | ||||
|         }, | ||||
|         VOYAGE: { | ||||
|             DEFAULT: 1024 | ||||
|         } | ||||
|     }, | ||||
|  | ||||
|     // Model-specific embedding dimensions for Ollama models | ||||
|     OLLAMA_MODEL_DIMENSIONS: { | ||||
|         "llama3": 8192, | ||||
|         "llama3.1": 8192, | ||||
|         "mistral": 8192, | ||||
|         "nomic": 768, | ||||
|         "mxbai": 1024, | ||||
|         "nomic-embed-text": 768, | ||||
|         "mxbai-embed-large": 1024, | ||||
|         "default": 384 | ||||
|     }, | ||||
|  | ||||
|     // Model-specific context windows for Ollama models | ||||
|     OLLAMA_MODEL_CONTEXT_WINDOWS: { | ||||
|         "llama3": 8192, | ||||
|         "llama3.1": 8192, | ||||
|         "llama3.2": 8192, | ||||
|         "mistral": 8192, | ||||
|         "nomic": 32768, | ||||
|         "mxbai": 32768, | ||||
|         "nomic-embed-text": 32768, | ||||
|         "mxbai-embed-large": 32768, | ||||
|         "default": 8192 | ||||
|     }, | ||||
|  | ||||
|     // Batch size configuration | ||||
|     BATCH_SIZE: { | ||||
|         OPENAI: 10,     // OpenAI can handle larger batches efficiently | ||||
| @@ -189,8 +132,7 @@ export const LLM_CONSTANTS = { | ||||
|     CHUNKING: { | ||||
|         DEFAULT_SIZE: 1500, | ||||
|         OLLAMA_SIZE: 1000, | ||||
|         DEFAULT_OVERLAP: 100, | ||||
|         MAX_SIZE_FOR_SINGLE_EMBEDDING: 5000 | ||||
|         DEFAULT_OVERLAP: 100 | ||||
|     }, | ||||
|  | ||||
|     // Search/similarity thresholds | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import type { ICacheManager, CachedNoteData, CachedQueryResults } from '../../in | ||||
|  * Provides a centralized caching system to avoid redundant operations | ||||
|  */ | ||||
| export class CacheManager implements ICacheManager { | ||||
|     // Cache for recently used context to avoid repeated embedding lookups | ||||
|     // Cache for recently used context to avoid repeated lookups | ||||
|     private noteDataCache = new Map<string, CachedNoteData<unknown>>(); | ||||
|  | ||||
|     // Cache for recently used queries | ||||
|   | ||||
| @@ -1,37 +1 @@ | ||||
| import log from '../../../log.js'; | ||||
|  | ||||
| /** | ||||
|  * Manages embedding providers for context services | ||||
|  * Simplified since embedding functionality has been removed | ||||
|  */ | ||||
| export class ProviderManager { | ||||
|     /** | ||||
|      * Get the selected embedding provider based on user settings | ||||
|      * Returns null since embeddings have been removed | ||||
|      */ | ||||
|     async getSelectedEmbeddingProvider(): Promise<null> { | ||||
|         log.info('Embedding providers have been removed - returning null'); | ||||
|         return null; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Get all enabled embedding providers | ||||
|      * Returns empty array since embeddings have been removed | ||||
|      */ | ||||
|     async getEnabledEmbeddingProviders(): Promise<never[]> { | ||||
|         log.info('Embedding providers have been removed - returning empty array'); | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Check if embedding providers are available | ||||
|      * Returns false since embeddings have been removed | ||||
|      */ | ||||
|     isEmbeddingAvailable(): boolean { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Export singleton instance | ||||
| export const providerManager = new ProviderManager(); | ||||
| export default providerManager; | ||||
| // This file has been removed as embedding functionality has been completely removed from the codebase | ||||
|   | ||||
| @@ -11,7 +11,6 @@ | ||||
|  */ | ||||
|  | ||||
| import log from '../../../log.js'; | ||||
| import providerManager from '../modules/provider_manager.js'; | ||||
| import cacheManager from '../modules/cache_manager.js'; | ||||
| import queryProcessor from './query_processor.js'; | ||||
| import contextFormatter from '../modules/context_formatter.js'; | ||||
| @@ -56,17 +55,11 @@ export class ContextService { | ||||
|  | ||||
|         this.initPromise = (async () => { | ||||
|             try { | ||||
|                 // Initialize provider | ||||
|                 const provider = await providerManager.getSelectedEmbeddingProvider(); | ||||
|                 if (!provider) { | ||||
|                     throw new Error(`No embedding provider available. Could not initialize context service.`); | ||||
|                 } | ||||
|  | ||||
|                 // Agent tools are already initialized in the AIServiceManager constructor | ||||
|                 // No need to initialize them again | ||||
|  | ||||
|                 this.initialized = true; | ||||
|                 log.info(`Context service initialized - embeddings disabled`); | ||||
|                 log.info(`Context service initialized`); | ||||
|             } catch (error: unknown) { | ||||
|                 const errorMessage = error instanceof Error ? error.message : String(error); | ||||
|                 log.error(`Failed to initialize context service: ${errorMessage}`); | ||||
| @@ -177,10 +170,9 @@ export class ContextService { | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Step 3: Find relevant notes using basic text search (since embeddings are removed) | ||||
|             // This will use traditional note search instead of vector similarity | ||||
|             log.info("Using traditional search instead of embedding-based search"); | ||||
|              | ||||
|             // Step 3: Find relevant notes using traditional search | ||||
|             log.info("Using traditional search for note discovery"); | ||||
|  | ||||
|             // Use fallback context based on the context note if provided | ||||
|             if (contextNoteId) { | ||||
|                 try { | ||||
| @@ -194,7 +186,7 @@ export class ContextService { | ||||
|                             similarity: 1.0, | ||||
|                             content: content || "" | ||||
|                         }]; | ||||
|                          | ||||
|  | ||||
|                         // Add child notes as additional context | ||||
|                         const childNotes = contextNote.getChildNotes().slice(0, maxResults - 1); | ||||
|                         for (const child of childNotes) { | ||||
| @@ -215,13 +207,10 @@ export class ContextService { | ||||
|             log.info(`Final combined results: ${relevantNotes.length} relevant notes`); | ||||
|  | ||||
|             // Step 4: Build context from the notes | ||||
|             const provider = await providerManager.getSelectedEmbeddingProvider(); | ||||
|             const providerId = 'default'; // Provider is always null since embeddings removed | ||||
|  | ||||
|             const context = await contextFormatter.buildContextFromNotes( | ||||
|                 relevantNotes, | ||||
|                 userQuestion, | ||||
|                 providerId | ||||
|                 'default' | ||||
|             ); | ||||
|  | ||||
|             // Step 5: Add agent tools context if requested | ||||
|   | ||||
| @@ -60,7 +60,6 @@ export interface IContextFormatter { | ||||
|  */ | ||||
| export interface ILLMService { | ||||
|   sendMessage(message: string, options?: Record<string, unknown>): Promise<string>; | ||||
|   generateEmbedding?(text: string): Promise<number[]>; | ||||
|   streamMessage?(message: string, callback: (text: string) => void, options?: Record<string, unknown>): Promise<string>; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -36,16 +36,6 @@ export interface OllamaError extends LLMServiceError { | ||||
|   code?: string; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Embedding-specific error interface | ||||
|  */ | ||||
| export interface EmbeddingError extends LLMServiceError { | ||||
|   provider: string; | ||||
|   model?: string; | ||||
|   batchSize?: number; | ||||
|   isRetryable: boolean; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Guard function to check if an error is a specific type of error | ||||
|  */ | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import aiServiceManager from './ai_service_manager.js'; | ||||
|  | ||||
| /** | ||||
|  * Service for fetching and caching model capabilities | ||||
|  * Simplified to only handle chat models since embeddings have been removed | ||||
|  * Handles chat model capabilities | ||||
|  */ | ||||
| export class ModelCapabilitiesService { | ||||
|     // Cache model capabilities | ||||
| @@ -24,10 +24,10 @@ export class ModelCapabilitiesService { | ||||
|  | ||||
|         // Get from static definitions or service | ||||
|         const capabilities = await this.fetchChatModelCapabilities(modelName); | ||||
|          | ||||
|  | ||||
|         // Cache the result | ||||
|         this.capabilitiesCache.set(`chat:${modelName}`, capabilities); | ||||
|          | ||||
|  | ||||
|         return capabilities; | ||||
|     } | ||||
|  | ||||
| @@ -82,4 +82,4 @@ export class ModelCapabilitiesService { | ||||
|  | ||||
| // Export singleton instance | ||||
| export const modelCapabilitiesService = new ModelCapabilitiesService(); | ||||
| export default modelCapabilitiesService; | ||||
| export default modelCapabilitiesService; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import { ModelSelectionStage } from './stages/model_selection_stage.js'; | ||||
| import { LLMCompletionStage } from './stages/llm_completion_stage.js'; | ||||
| import { ResponseProcessingStage } from './stages/response_processing_stage.js'; | ||||
| import { ToolCallingStage } from './stages/tool_calling_stage.js'; | ||||
| // VectorSearchStage removed along with embedding functionality | ||||
| // Traditional search is used instead of vector search | ||||
| import toolRegistry from '../tools/tool_registry.js'; | ||||
| import toolInitializer from '../tools/tool_initializer.js'; | ||||
| import log from '../../log.js'; | ||||
| @@ -29,7 +29,7 @@ export class ChatPipeline { | ||||
|         llmCompletion: LLMCompletionStage; | ||||
|         responseProcessing: ResponseProcessingStage; | ||||
|         toolCalling: ToolCallingStage; | ||||
|         // vectorSearch removed with embedding functionality | ||||
|         // traditional search is used instead of vector search | ||||
|     }; | ||||
|  | ||||
|     config: ChatPipelineConfig; | ||||
| @@ -50,7 +50,7 @@ export class ChatPipeline { | ||||
|             llmCompletion: new LLMCompletionStage(), | ||||
|             responseProcessing: new ResponseProcessingStage(), | ||||
|             toolCalling: new ToolCallingStage(), | ||||
|             // vectorSearch removed with embedding functionality | ||||
|             // traditional search is used instead of vector search | ||||
|         }; | ||||
|  | ||||
|         // Set default configuration values | ||||
| @@ -307,7 +307,7 @@ export class ChatPipeline { | ||||
|  | ||||
|                     // Forward to callback with original chunk data in case it contains additional information | ||||
|                     streamCallback(processedChunk.text, processedChunk.done, chunk); | ||||
|                      | ||||
|  | ||||
|                     // Mark that we have streamed content to prevent duplication | ||||
|                     hasStreamedContent = true; | ||||
|                 }); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| /** | ||||
|  * Provider Validation Service | ||||
|  *  | ||||
|  * | ||||
|  * Validates AI provider configurations before initializing the chat system. | ||||
|  * This prevents startup errors when AI is enabled but providers are misconfigured. | ||||
|  */ | ||||
| @@ -58,7 +58,7 @@ async function checkChatProviderConfigs(result: ProviderValidationResult): Promi | ||||
|         // Check OpenAI chat provider | ||||
|         const openaiApiKey = await options.getOption('openaiApiKey'); | ||||
|         const openaiBaseUrl = await options.getOption('openaiBaseUrl'); | ||||
|          | ||||
|  | ||||
|         if (openaiApiKey || openaiBaseUrl) { | ||||
|             result.validChatProviders.push('openai'); | ||||
|             log.info("OpenAI chat provider configuration available"); | ||||
| @@ -83,16 +83,6 @@ async function checkChatProviderConfigs(result: ProviderValidationResult): Promi | ||||
|     } | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Check if we have at least one valid embedding provider available | ||||
|  * Returns false since embeddings have been removed | ||||
|  */ | ||||
| export async function getEmbeddingProviderAvailability(): Promise<boolean> { | ||||
|     log.info("Embedding providers have been removed, returning false"); | ||||
|     return false; | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     validateProviders, | ||||
|     getEmbeddingProviderAvailability | ||||
| }; | ||||
|     validateProviders | ||||
| }; | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import attributes from '../../attributes.js'; | ||||
| import aiServiceManager from '../ai_service_manager.js'; | ||||
| import { SEARCH_CONSTANTS } from '../constants/search_constants.js'; | ||||
| import searchService from '../../search/services/search.js'; | ||||
| // Define types locally since embeddings are no longer available | ||||
| // Define types locally for relationship tool | ||||
| interface Backlink { | ||||
|     noteId: string; | ||||
|     title: string; | ||||
| @@ -278,7 +278,7 @@ export class RelationshipTool implements ToolHandler { | ||||
|  | ||||
|             // Create search queries from the note title and content | ||||
|             const searchQueries = [title]; | ||||
|              | ||||
|  | ||||
|             // Extract key terms from content if available | ||||
|             if (content && typeof content === 'string') { | ||||
|                 // Extract meaningful words from content (filter out common words) | ||||
| @@ -288,7 +288,7 @@ export class RelationshipTool implements ToolHandler { | ||||
|                     .filter(word => word.length > 3) | ||||
|                     .filter(word => !/^(the|and|but|for|are|from|they|been|have|this|that|with|will|when|where|what|how)$/.test(word)) | ||||
|                     .slice(0, 10); // Take first 10 meaningful words | ||||
|                  | ||||
|  | ||||
|                 if (contentWords.length > 0) { | ||||
|                     searchQueries.push(contentWords.join(' ')); | ||||
|                 } | ||||
| @@ -301,11 +301,11 @@ export class RelationshipTool implements ToolHandler { | ||||
|  | ||||
|             for (const query of searchQueries) { | ||||
|                 try { | ||||
|                     const results = searchService.searchNotes(query, {  | ||||
|                     const results = searchService.searchNotes(query, { | ||||
|                         includeArchivedNotes: false, | ||||
|                         fastSearch: false // Use full search for better results | ||||
|                     }); | ||||
|                      | ||||
|  | ||||
|                     // Add results to our map (avoiding duplicates) | ||||
|                     for (const note of results.slice(0, limit * 2)) { // Get more to account for duplicates | ||||
|                         if (note.noteId !== sourceNote.noteId && !note.isDeleted) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user