feat(ai): wire up workspace RAG bulk reindex + manual "Reindex now"

The WORKSPACE_CREATE_EMBEDDINGS / WORKSPACE_DELETE_EMBEDDINGS jobs were
enqueued (on AI Search enable/disable) but had no AI_QUEUE handler, so
existing pages were never indexed ("Indexed 0 of N pages") and disabling
never purged embeddings.

- EmbeddingProcessor: handle WORKSPACE_CREATE_EMBEDDINGS (bulk reindex all
  live pages) and WORKSPACE_DELETE_EMBEDDINGS (purge workspace embeddings)
- EmbeddingIndexerService: add reindexWorkspace() (skips when embeddings
  unconfigured; per-page error isolation) and removeWorkspace()
- PageRepo.getIdsByWorkspace(), PageEmbeddingRepo.deleteByWorkspace()
- AiSettingsService.reindex() + admin-only POST /workspace/ai-settings/reindex
- Frontend: "Reindex now" button, service call and mutation
- Stable per-workspace jobId with remove-before-add so a stale job can't
  block future reindexes; cancel the delayed purge on enable/reindex so it
  can't wipe freshly-built embeddings
This commit is contained in:
vvzvlad
2026-06-18 02:15:18 +03:00
parent 4cf6b73d3e
commit 52e19fe678
12 changed files with 253 additions and 16 deletions

View File

@@ -18,6 +18,7 @@ import { useTranslation } from "react-i18next";
import useUserRole from "@/hooks/use-user-role.tsx";
import {
useAiSettingsQuery,
useReindexAiEmbeddingsMutation,
useTestAiConnectionMutation,
useUpdateAiSettingsMutation,
} from "@/features/workspace/queries/ai-settings-query.ts";
@@ -50,6 +51,7 @@ export default function AiProviderSettings() {
const { data: settings, isLoading } = useAiSettingsQuery(isAdmin);
const updateMutation = useUpdateAiSettingsMutation();
const testMutation = useTestAiConnectionMutation();
const reindexMutation = useReindexAiEmbeddingsMutation();
// Whether a key is currently stored server-side (drives the placeholder).
const [hasApiKey, setHasApiKey] = useState(false);
@@ -258,12 +260,24 @@ export default function AiProviderSettings() {
)}
{settings && (
<Text size="sm" c="dimmed" mt={-8}>
{t("Indexed {{indexed}} of {{total}} pages", {
indexed: settings.indexedPages ?? 0,
total: settings.totalPages ?? 0,
})}
</Text>
<Group justify="space-between" mt={-8}>
<Text size="sm" c="dimmed">
{t("Indexed {{indexed}} of {{total}} pages", {
indexed: settings.indexedPages ?? 0,
total: settings.totalPages ?? 0,
})}
</Text>
{isAdmin && (
<Button
variant="subtle"
size="compact-sm"
onClick={() => reindexMutation.mutate()}
loading={reindexMutation.isPending}
>
{t("Reindex now")}
</Button>
)}
</Group>
)}
<Textarea

View File

@@ -8,6 +8,7 @@ import {
getAiSettings,
updateAiSettings,
testAiConnection,
reindexAiEmbeddings,
IAiSettings,
IAiSettingsUpdate,
IAiTestResult,
@@ -52,3 +53,23 @@ export function useTestAiConnectionMutation() {
mutationFn: () => testAiConnection(),
});
}
export function useReindexAiEmbeddingsMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<IAiSettings, Error, void>({
mutationFn: () => reindexAiEmbeddings(),
onSuccess: () => {
notifications.show({ message: t("Reindexing started") });
queryClient.invalidateQueries({ queryKey: aiSettingsKey });
},
onError: (error) => {
const errorMessage = error["response"]?.data?.message;
notifications.show({
message: errorMessage ?? t("Failed to start reindexing"),
color: "red",
});
},
});
}

View File

@@ -60,3 +60,8 @@ export async function testAiConnection(): Promise<IAiTestResult> {
const req = await api.post<IAiTestResult>("/workspace/ai-settings/test");
return req.data;
}
export async function reindexAiEmbeddings(): Promise<IAiSettings> {
const req = await api.post<IAiSettings>("/workspace/ai-settings/reindex");
return req.data;
}