diff --git a/packages/frontend/component/src/ui/progress/progress.tsx b/packages/frontend/component/src/ui/progress/progress.tsx
index debf3656e5..bcc42e1fea 100644
--- a/packages/frontend/component/src/ui/progress/progress.tsx
+++ b/packages/frontend/component/src/ui/progress/progress.tsx
@@ -15,6 +15,7 @@ export interface ProgressProps {
readonly?: boolean;
className?: string;
style?: React.CSSProperties;
+ testId?: string;
}
export const Progress = ({
@@ -24,9 +25,14 @@ export const Progress = ({
readonly,
className,
style,
+ testId,
}: ProgressProps) => {
return (
-
+
(false);
error$ = new LiveData(null);
@@ -50,6 +61,11 @@ export class Embedding extends Entity {
isEnabledLoading$ = new LiveData(false);
isAttachmentsLoading$ = new LiveData(false);
isIgnoredDocsLoading$ = new LiveData(false);
+ embeddingProgress$ = new LiveData(null);
+ isEmbeddingProgressLoading$ = new LiveData(false);
+
+ private readonly EMBEDDING_PROGRESS_POLL_INTERVAL = 3000;
+ private readonly stopEmbeddingProgress$ = new Subject();
constructor(
private readonly workspaceService: WorkspaceService,
@@ -59,6 +75,7 @@ export class Embedding extends Entity {
this.getEnabled();
this.getAttachments({ first: COUNT_PER_PAGE, after: null });
this.getIgnoredDocs();
+ this.getEmbeddingProgress();
}
getEnabled = effect(
@@ -228,6 +245,48 @@ export class Embedding extends Entity {
})
);
+ startEmbeddingProgressPolling() {
+ this.stopEmbeddingProgressPolling();
+ this.getEmbeddingProgress();
+ }
+
+ stopEmbeddingProgressPolling() {
+ this.stopEmbeddingProgress$.next();
+ }
+
+ getEmbeddingProgress = effect(
+ exhaustMap(() => {
+ return interval(this.EMBEDDING_PROGRESS_POLL_INTERVAL).pipe(
+ takeUntil(this.stopEmbeddingProgress$),
+ switchMap(() =>
+ fromPromise(signal =>
+ this.store.getEmbeddingProgress(
+ this.workspaceService.workspace.id,
+ signal
+ )
+ ).pipe(
+ smartRetry(),
+ mergeMap(value => {
+ this.embeddingProgress$.next(value);
+ if (value && value.embedded === value.total) {
+ this.stopEmbeddingProgressPolling();
+ }
+ return EMPTY;
+ }),
+ catchErrorInto(this.error$, error => {
+ logger.error(
+ 'Failed to fetch workspace embedding progress',
+ error
+ );
+ }),
+ onStart(() => this.isEmbeddingProgressLoading$.setValue(true)),
+ onComplete(() => this.isEmbeddingProgressLoading$.setValue(false))
+ )
+ )
+ );
+ })
+ );
+
override dispose(): void {
this.getEnabled.unsubscribe();
this.getAttachments.unsubscribe();
@@ -236,5 +295,7 @@ export class Embedding extends Entity {
this.addAttachments.unsubscribe();
this.removeAttachment.unsubscribe();
this.setEnabled.unsubscribe();
+ this.stopEmbeddingProgress$.next();
+ this.getEmbeddingProgress.unsubscribe();
}
}
diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/stores/embedding.ts b/packages/frontend/core/src/modules/workspace-indexer-embedding/stores/embedding.ts
index 1b02db92fb..59446c2445 100644
--- a/packages/frontend/core/src/modules/workspace-indexer-embedding/stores/embedding.ts
+++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/stores/embedding.ts
@@ -5,6 +5,7 @@ import {
getAllWorkspaceEmbeddingIgnoredDocsQuery,
getWorkspaceConfigQuery,
getWorkspaceEmbeddingFilesQuery,
+ getWorkspaceEmbeddingStatusQuery,
type PaginationInput,
removeWorkspaceEmbeddingFilesMutation,
removeWorkspaceEmbeddingIgnoredDocsMutation,
@@ -175,4 +176,19 @@ export class EmbeddingStore extends Store {
});
return data.workspace.embedding.files;
}
+
+ async getEmbeddingProgress(workspaceId: string, signal?: AbortSignal) {
+ if (!this.workspaceServerService.server) {
+ throw new Error('No Server');
+ }
+
+ const data = await this.workspaceServerService.server.gql({
+ query: getWorkspaceEmbeddingStatusQuery,
+ variables: {
+ workspaceId,
+ },
+ context: { signal },
+ });
+ return data.queryWorkspaceEmbeddingStatus;
+ }
}
diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-progress.tsx b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-progress.tsx
new file mode 100644
index 0000000000..ffbf5f4f55
--- /dev/null
+++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-progress.tsx
@@ -0,0 +1,60 @@
+import { Progress } from '@affine/component';
+import { useI18n } from '@affine/i18n';
+
+import { embeddingProgress, embeddingProgressTitle } from './styles-css';
+
+interface EmbeddingProgressProps {
+ status: {
+ embedded: number;
+ total: number;
+ } | null;
+}
+
+const EmbeddingProgress: React.FC = ({ status }) => {
+ const t = useI18n();
+
+ const loading = status === null;
+
+ const percent = loading
+ ? 0
+ : status.total === 0
+ ? 1
+ : status.embedded / status.total;
+ const progress = Math.round(percent * 100);
+ const synced = percent === 1;
+
+ return (
+
+
+
+ {loading
+ ? t[
+ 'com.affine.settings.workspace.indexer-embedding.embedding.progress.loading-sync-status'
+ ]()
+ : synced
+ ? t[
+ 'com.affine.settings.workspace.indexer-embedding.embedding.progress.synced'
+ ]()
+ : t[
+ 'com.affine.settings.workspace.indexer-embedding.embedding.progress.syncing'
+ ]()}
+
+ {loading ? null : (
+
{`${status.embedded}/${status.total}`}
+ )}
+
+
+
+ );
+};
+
+export default EmbeddingProgress;
diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-settings.tsx b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-settings.tsx
index 1617f2fa4a..ce672bc69b 100644
--- a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-settings.tsx
+++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-settings.tsx
@@ -9,10 +9,11 @@ import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
import type React from 'react';
-import { useCallback, useMemo } from 'react';
+import { useCallback, useEffect, useMemo } from 'react';
import { EmbeddingService } from '../services/embedding';
import { Attachments } from './attachments';
+import EmbeddingProgress from './embedding-progress';
import { IgnoredDocs } from './ignored-docs';
interface EmbeddingSettingsProps {}
@@ -23,6 +24,10 @@ export const EmbeddingSettings: React.FC = () => {
const embeddingEnabled = useLiveData(embeddingService.embedding.enabled$);
const attachments = useLiveData(embeddingService.embedding.attachments$);
const ignoredDocs = useLiveData(embeddingService.embedding.ignoredDocs$);
+ const embeddingProgress = useLiveData(
+ embeddingService.embedding.embeddingProgress$
+ );
+
const isIgnoredDocsLoading = useLiveData(
embeddingService.embedding.isIgnoredDocsLoading$
);
@@ -34,7 +39,6 @@ export const EmbeddingSettings: React.FC = () => {
[attachments]
);
const ignoredDocNodes = ignoredDocs;
-
const workspaceDialogService = useService(WorkspaceDialogService);
const handleEmbeddingToggle = useCallback(
@@ -94,6 +98,13 @@ export const EmbeddingSettings: React.FC = () => {
embeddingService.embedding,
]);
+ useEffect(() => {
+ embeddingService.embedding.startEmbeddingProgressPolling();
+ return () => {
+ embeddingService.embedding.stopEmbeddingProgressPolling();
+ };
+ }, [embeddingService.embedding]);
+
return (
<>
= () => {
/>
+
+
+
+
{
await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, true);
});
+ test('should show embedding progress', async ({
+ loggedInPage: page,
+ utils,
+ }) => {
+ await utils.settings.enableWorkspaceEmbedding(page);
+ await page.getByTestId('embedding-progress-wrapper');
+
+ const progress = await page.getByTestId('embedding-progress');
+ // wait for the progress to be loading
+ const title = await page.getByTestId('embedding-progress-title');
+ await expect(title).toHaveText(/Loading sync status/i);
+ await expect(progress).not.toBeVisible();
+
+ const count = await page.getByTestId('embedding-progress-count');
+ await expect(count).toHaveText(/\d+\/\d+/);
+ await expect(progress).toBeVisible();
+ });
+
test('should allow manual attachment upload for embedding', async ({
loggedInPage: page,
utils,