diff --git a/src/public/app/widgets/type_widgets/options/ai_settings.ts b/src/public/app/widgets/type_widgets/options/ai_settings.ts index de76f5de3..6f43b4319 100644 --- a/src/public/app/widgets/type_widgets/options/ai_settings.ts +++ b/src/public/app/widgets/type_widgets/options/ai_settings.ts @@ -47,6 +47,7 @@ interface FailedEmbeddingNotes { export default class AiSettingsWidget extends OptionsWidget { private statsRefreshInterval: NodeJS.Timeout | null = null; + private indexRebuildRefreshInterval: NodeJS.Timeout | null = null; private readonly STATS_REFRESH_INTERVAL = 5000; // 5 seconds doRender() { @@ -243,6 +244,17 @@ export default class AiSettingsWidget extends OptionsWidget { ${t("ai_llm.reprocess_index")}
${t("ai_llm.reprocess_index_description")}
+ + +
@@ -476,7 +488,10 @@ export default class AiSettingsWidget extends OptionsWidget { try { await server.post('embeddings/rebuild-index'); toastService.showMessage(t("ai_llm.reprocess_index_started")); - // Refresh stats after reprocessing starts + // Start tracking index rebuild progress + await this.refreshIndexRebuildStatus(); + + // Also refresh embedding stats since they'll update as embeddings are processed await this.refreshEmbeddingStats(); } catch (error) { console.error("Error rebuilding index:", error); @@ -567,6 +582,9 @@ export default class AiSettingsWidget extends OptionsWidget { this.$widget.find('.embedding-section').is(':visible')) { await this.refreshEmbeddingStats(true); + // Also check index rebuild status + await this.refreshIndexRebuildStatus(true); + // Also update failed embeddings list periodically await this.updateFailedEmbeddingsList(); } @@ -581,6 +599,11 @@ export default class AiSettingsWidget extends OptionsWidget { clearInterval(this.statsRefreshInterval); this.statsRefreshInterval = null; } + + if (this.indexRebuildRefreshInterval) { + clearInterval(this.indexRebuildRefreshInterval); + this.indexRebuildRefreshInterval = null; + } } // Clean up when the widget is removed @@ -699,6 +722,11 @@ export default class AiSettingsWidget extends OptionsWidget { if (stats.failedNotesCount > 0 && !silent) { await this.updateFailedEmbeddingsList(); } + + // Also check index rebuild status if not in silent mode + if (!silent) { + await this.refreshIndexRebuildStatus(silent); + } } } catch (error) { console.error("Error fetching embedding stats:", error); @@ -715,6 +743,83 @@ export default class AiSettingsWidget extends OptionsWidget { } } + /** + * Refresh the index rebuild status + */ + async refreshIndexRebuildStatus(silent = false) { + if (!this.$widget) return; + + try { + // Get the current status of index rebuilding + const response = await server.get('embeddings/index-rebuild-status') as { + success: boolean, + status: { + inProgress: boolean, + progress: number, + total: number, + current: number + } + }; + + if (response && response.success) { + const status = response.status; + const $progressContainer = this.$widget.find('.index-rebuild-progress-container'); + const $progressBar = this.$widget.find('.index-rebuild-progress'); + const $statusText = this.$widget.find('.index-rebuild-status-text'); + + // Only show the progress container if rebuild is in progress + if (status.inProgress) { + $progressContainer.show(); + } else if (status.progress === 100) { + // Show for 10 seconds after completion, then hide + $progressContainer.show(); + setTimeout(() => { + $progressContainer.fadeOut('slow'); + }, 10000); + } else if (status.progress === 0) { + // Hide if no rebuild has been started + $progressContainer.hide(); + } + + // Update progress bar + $progressBar.css('width', `${status.progress}%`); + $progressBar.attr('aria-valuenow', status.progress.toString()); + $progressBar.text(`${status.progress}%`); + + // Update status text + if (status.inProgress) { + $statusText.text(t("ai_llm.index_rebuilding", { percentage: status.progress })); + + // Apply animated style for active progress + $progressBar.addClass('progress-bar-striped progress-bar-animated bg-info'); + $progressBar.removeClass('bg-success'); + } else if (status.progress === 100) { + $statusText.text(t("ai_llm.index_rebuild_complete")); + + // Apply success style for completed progress + $progressBar.removeClass('progress-bar-striped progress-bar-animated bg-info'); + $progressBar.addClass('bg-success'); + } + + // Start a refresh interval if in progress + if (status.inProgress && !this.indexRebuildRefreshInterval) { + this.indexRebuildRefreshInterval = setInterval(() => { + this.refreshIndexRebuildStatus(true); + }, this.STATS_REFRESH_INTERVAL); + } else if (!status.inProgress && this.indexRebuildRefreshInterval) { + // Clear the interval if rebuild is complete + clearInterval(this.indexRebuildRefreshInterval); + this.indexRebuildRefreshInterval = null; + } + } + } catch (error) { + console.error("Error fetching index rebuild status:", error); + if (!silent) { + toastService.showError(t("ai_llm.index_rebuild_status_error")); + } + } + } + async updateFailedEmbeddingsList() { if (!this.$widget) return; diff --git a/src/public/translations/en/translation.json b/src/public/translations/en/translation.json index b68881c1c..c5c144953 100644 --- a/src/public/translations/en/translation.json +++ b/src/public/translations/en/translation.json @@ -1186,6 +1186,11 @@ "reprocess_index_started": "Index rebuilding started in the background", "reprocess_index_error": "Error rebuilding search index", + "index_rebuild_progress": "Index Rebuild Progress", + "index_rebuilding": "Rebuilding index ({{percentage}}%)", + "index_rebuild_complete": "Index rebuild complete", + "index_rebuild_status_error": "Error checking index rebuild status", + "embedding_statistics": "Embedding Statistics", "total_notes": "Total Notes", "processed_notes": "Processed Notes", diff --git a/src/routes/api/embeddings.ts b/src/routes/api/embeddings.ts index 69253bd9c..d15a6b9c9 100644 --- a/src/routes/api/embeddings.ts +++ b/src/routes/api/embeddings.ts @@ -279,6 +279,18 @@ async function rebuildIndex(req: Request, res: Response) { }; } +/** + * Get the current index rebuild status + */ +async function getIndexRebuildStatus(req: Request, res: Response) { + const status = indexService.getIndexRebuildStatus(); + + return { + success: true, + status + }; +} + export default { findSimilarNotes, searchByText, @@ -290,5 +302,6 @@ export default { getFailedNotes, retryFailedNote, retryAllFailedNotes, - rebuildIndex + rebuildIndex, + getIndexRebuildStatus }; diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 251544817..d6b77b437 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -384,6 +384,7 @@ function register(app: express.Application) { apiRoute(PST, "/api/embeddings/retry/:noteId", embeddingsRoute.retryFailedNote); apiRoute(PST, "/api/embeddings/retry-all-failed", embeddingsRoute.retryAllFailedNotes); apiRoute(PST, "/api/embeddings/rebuild-index", embeddingsRoute.rebuildIndex); + apiRoute(GET, "/api/embeddings/index-rebuild-status", embeddingsRoute.getIndexRebuildStatus); // LLM chat session management endpoints apiRoute(PST, "/api/llm/sessions", llmRoute.createSession); diff --git a/src/services/llm/embeddings/queue.ts b/src/services/llm/embeddings/queue.ts index 47be9e2f9..5218f06dd 100644 --- a/src/services/llm/embeddings/queue.ts +++ b/src/services/llm/embeddings/queue.ts @@ -8,6 +8,7 @@ import { getNoteEmbeddingContext } from "./content_processing.js"; import { deleteNoteEmbeddings } from "./storage.js"; import type { QueueItem } from "./types.js"; import { getChunkingOperations } from "./chunking_interface.js"; +import indexService from '../index_service.js'; /** * Queues a note for embedding update @@ -176,6 +177,9 @@ export async function processEmbeddingQueue() { return; } + // Track successfully processed notes count for progress reporting + let processedCount = 0; + for (const note of notes) { try { const noteData = note as unknown as QueueItem; @@ -248,6 +252,8 @@ export async function processEmbeddingQueue() { "DELETE FROM embedding_queue WHERE noteId = ?", [noteData.noteId] ); + // Count as successfully processed + processedCount++; } else { // If all providers failed, mark as failed but keep in queue await sql.execute(` @@ -286,4 +292,9 @@ export async function processEmbeddingQueue() { } } } + + // Update the index rebuild progress if any notes were processed + if (processedCount > 0) { + indexService.updateIndexRebuildProgress(processedCount); + } } diff --git a/src/services/llm/index_service.ts b/src/services/llm/index_service.ts index e75332dfb..3ae5ff20f 100644 --- a/src/services/llm/index_service.ts +++ b/src/services/llm/index_service.ts @@ -18,6 +18,7 @@ import { ContextExtractor } from "./context/index.js"; import eventService from "../events.js"; import type { NoteEmbeddingContext } from "./embeddings/embeddings_interface.js"; import type { OptionDefinitions } from "../options_interface.js"; +import sql from "../sql.js"; class IndexService { private initialized = false; @@ -25,6 +26,12 @@ class IndexService { private contextExtractor = new ContextExtractor(); private automaticIndexingInterval?: NodeJS.Timeout; + // Index rebuilding tracking + private indexRebuildInProgress = false; + private indexRebuildProgress = 0; + private indexRebuildTotal = 0; + private indexRebuildCurrent = 0; + // Configuration private defaultQueryDepth = 2; private maxNotesPerQuery = 10; @@ -195,6 +202,13 @@ class IndexService { try { this.indexingInProgress = true; + this.indexRebuildInProgress = true; + this.indexRebuildProgress = 0; + this.indexRebuildCurrent = 0; + + // Reset index rebuild progress + const totalNotes = await sql.getValue("SELECT COUNT(*) FROM notes WHERE isDeleted = 0") as number; + this.indexRebuildTotal = totalNotes; if (force) { // Force reindexing of all notes @@ -210,18 +224,55 @@ class IndexService { log.info("Full indexing initiated"); } else { log.info(`Skipping full indexing, already at ${stats.percentComplete}% completion`); + this.indexRebuildInProgress = false; + this.indexRebuildProgress = 100; } } return true; } catch (error: any) { log.error(`Error starting full indexing: ${error.message || "Unknown error"}`); + this.indexRebuildInProgress = false; return false; } finally { this.indexingInProgress = false; } } + /** + * Update index rebuild progress + * @param processed - Number of notes processed + */ + updateIndexRebuildProgress(processed: number) { + if (!this.indexRebuildInProgress) return; + + this.indexRebuildCurrent += processed; + + if (this.indexRebuildTotal > 0) { + this.indexRebuildProgress = Math.min( + Math.round((this.indexRebuildCurrent / this.indexRebuildTotal) * 100), + 100 + ); + } + + if (this.indexRebuildCurrent >= this.indexRebuildTotal) { + this.indexRebuildInProgress = false; + this.indexRebuildProgress = 100; + } + } + + /** + * Get the current index rebuild progress + */ + getIndexRebuildStatus() { + return { + inProgress: this.indexRebuildInProgress, + progress: this.indexRebuildProgress, + total: this.indexRebuildTotal, + current: this.indexRebuildCurrent + }; + } + /** * Run a batch indexing job for a limited number of notes * @param batchSize - Maximum number of notes to process