feat(ai-chat): show RAG indexing coverage in AI settings

Display "Indexed N of M pages" on the AI provider settings page so admins
can see how much of the wiki is covered by vector-RAG semantic search.

- page-embedding.repo: add countIndexedPages() — distinct non-deleted pages
  that have stored embeddings in the workspace
- page.repo: add countByWorkspace() — total non-deleted pages
- ai-settings.service: compute both counts in getMasked() (Promise.all) and
  return them with the masked settings; inject PageEmbeddingRepo + PageRepo
- MaskedAiSettings / IAiSettings: add indexedPages + totalPages
- ai-provider-settings: render a dimmed coverage line under "Embedding model"
- i18n: add the "Indexed {{indexed}} of {{total}} pages" key (en-US, ru-RU)
This commit is contained in:
vvzvlad
2026-06-17 23:18:51 +03:00
parent 48c158bb7e
commit 1f2d20244e
8 changed files with 68 additions and 1 deletions

View File

@@ -157,4 +157,28 @@ export class PageEmbeddingRepo {
distance: Number(row.distance),
}));
}
/**
* Count DISTINCT non-deleted pages that have at least one embedding row in this
* workspace — i.e. how many pages currently have stored embeddings.
*
* NOTE: this counts pages embedded by ANY model dimension, whereas
* `searchByEmbedding` only serves rows matching the active model's dimension.
* After switching the embedding model, this number can therefore exceed the
* set of pages actually reachable by search until those pages are re-indexed.
* It is an indexing-coverage indicator, not an exact searchable-page count.
*/
async countIndexedPages(workspaceId: string): Promise<number> {
const row = await this.db
.selectFrom('pageEmbeddings as pe')
.innerJoin('pages as p', 'p.id', 'pe.pageId')
.where('pe.workspaceId', '=', workspaceId)
// Exclude trashed pages and any soft-deleted embedding rows (defence in
// depth: embeddings are hard-deleted, so pe.deletedAt is normally null).
.where('p.deletedAt', 'is', null)
.where('pe.deletedAt', 'is', null)
.select((eb) => eb.fn.count('pe.pageId').distinct().as('count'))
.executeTakeFirst();
return Number(row?.count ?? 0);
}
}