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:
@@ -92,6 +92,7 @@
|
||||
"Import pages": "Import pages",
|
||||
"Import pages & space settings": "Import pages & space settings",
|
||||
"Importing pages": "Importing pages",
|
||||
"Indexed {{indexed}} of {{total}} pages": "Indexed {{indexed}} of {{total}} pages",
|
||||
"invalid invitation link": "invalid invitation link",
|
||||
"Invitation signup": "Invitation signup",
|
||||
"Invite by email": "Invite by email",
|
||||
|
||||
@@ -92,6 +92,7 @@
|
||||
"Import pages": "Импорт страниц",
|
||||
"Import pages & space settings": "Импорт страниц и настройки пространства",
|
||||
"Importing pages": "Импортирование страниц",
|
||||
"Indexed {{indexed}} of {{total}} pages": "Проиндексировано {{indexed}} из {{total}} страниц",
|
||||
"invalid invitation link": "недействительная ссылка-приглашение",
|
||||
"Invitation signup": "Регистрация по приглашению",
|
||||
"Invite by email": "Пригласить по электронной почте",
|
||||
|
||||
@@ -188,6 +188,15 @@ export default function AiProviderSettings() {
|
||||
{...form.getInputProps("embeddingModel")}
|
||||
/>
|
||||
|
||||
{settings && (
|
||||
<Text size="sm" c="dimmed" mt={-8}>
|
||||
{t("Indexed {{indexed}} of {{total}} pages", {
|
||||
indexed: settings.indexedPages ?? 0,
|
||||
total: settings.totalPages ?? 0,
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
label={t("System message")}
|
||||
description={t(
|
||||
|
||||
@@ -12,6 +12,9 @@ export interface IAiSettings {
|
||||
baseUrl?: string;
|
||||
systemPrompt?: string;
|
||||
hasApiKey: boolean;
|
||||
// RAG indexing coverage (pages indexed for semantic search).
|
||||
indexedPages: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
// Update payload. Key semantics:
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,6 +181,20 @@ export class PageRepo {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count non-deleted pages in a workspace. Used by the AI settings page to show
|
||||
* RAG indexing coverage ("N of M pages indexed").
|
||||
*/
|
||||
async countByWorkspace(workspaceId: string): Promise<number> {
|
||||
const row = await this.db
|
||||
.selectFrom('pages')
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.where('deletedAt', 'is', null)
|
||||
.select((eb) => eb.fn.countAll().as('count'))
|
||||
.executeTakeFirst();
|
||||
return Number(row?.count ?? 0);
|
||||
}
|
||||
|
||||
async deletePage(pageId: string): Promise<void> {
|
||||
let query = this.db.deleteFrom('pages');
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
|
||||
import { AiProviderCredentialsRepo } from '@docmost/db/repos/ai-chat/ai-provider-credentials.repo';
|
||||
import { PageEmbeddingRepo } from '@docmost/db/repos/ai-chat/page-embedding.repo';
|
||||
import { PageRepo } from '@docmost/db/repos/page/page.repo';
|
||||
import { SecretBoxService } from '../crypto/secret-box';
|
||||
import {
|
||||
AiDriver,
|
||||
@@ -36,6 +38,8 @@ export class AiSettingsService {
|
||||
constructor(
|
||||
private readonly workspaceRepo: WorkspaceRepo,
|
||||
private readonly aiProviderCredentialsRepo: AiProviderCredentialsRepo,
|
||||
private readonly pageEmbeddingRepo: PageEmbeddingRepo,
|
||||
private readonly pageRepo: PageRepo,
|
||||
private readonly secretBox: SecretBoxService,
|
||||
) {}
|
||||
|
||||
@@ -82,7 +86,8 @@ export class AiSettingsService {
|
||||
|
||||
/**
|
||||
* Masked settings safe for admin clients. NEVER includes the key (even
|
||||
* encrypted); only `hasApiKey` for the current driver.
|
||||
* encrypted); only `hasApiKey` for the current driver. Also reports RAG
|
||||
* indexing coverage (`indexedPages`/`totalPages`) for the settings UI.
|
||||
*/
|
||||
async getMasked(workspaceId: string): Promise<MaskedAiSettings> {
|
||||
const provider = await this.readProvider(workspaceId);
|
||||
@@ -96,6 +101,11 @@ export class AiSettingsService {
|
||||
hasApiKey = !!creds?.apiKeyEnc;
|
||||
}
|
||||
|
||||
const [indexedPages, totalPages] = await Promise.all([
|
||||
this.pageEmbeddingRepo.countIndexedPages(workspaceId),
|
||||
this.pageRepo.countByWorkspace(workspaceId),
|
||||
]);
|
||||
|
||||
return {
|
||||
driver: provider.driver,
|
||||
chatModel: provider.chatModel,
|
||||
@@ -103,6 +113,8 @@ export class AiSettingsService {
|
||||
baseUrl: provider.baseUrl,
|
||||
systemPrompt: provider.systemPrompt,
|
||||
hasApiKey,
|
||||
indexedPages,
|
||||
totalPages,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,7 @@ export interface MaskedAiSettings {
|
||||
baseUrl?: string;
|
||||
systemPrompt?: string;
|
||||
hasApiKey: boolean;
|
||||
// RAG indexing coverage for the settings UI.
|
||||
indexedPages: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user