From d70f09b498a764332a25d87287d59fad0f5f213c Mon Sep 17 00:00:00 2001 From: yoyoyohamapi <8338436+yoyoyohamapi@users.noreply.github.com> Date: Wed, 21 May 2025 05:07:13 +0000 Subject: [PATCH] feat(core): embedding progress (#12367) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### TL;DR feat: show embedding progress in settings panel ![截屏2025-05-19 20.25.19.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/59d8f9ef-0876-4ed5-9c09-db12686adb47.png) ### What changed * show embedding progress in settings panel * polling embedding status based on RxJS ## Summary by CodeRabbit - **New Features** - Added real-time embedding progress tracking and display in embedding settings, including a visual progress bar and status messages. - Introduced localized text for embedding progress statuses. - Added an optional test ID attribute to the progress bar component for improved testing. - **Style** - Added new styles for embedding progress UI elements. - **Tests** - Added an end-to-end test to verify embedding progress is displayed correctly in the settings UI. --- .../component/src/ui/progress/progress.tsx | 8 ++- .../entities/embedding.ts | 65 ++++++++++++++++++- .../stores/embedding.ts | 16 +++++ .../view/embedding-progress.tsx | 60 +++++++++++++++++ .../view/embedding-settings.tsx | 24 ++++++- .../view/styles-css.ts | 19 ++++++ packages/frontend/i18n/src/i18n.gen.ts | 16 +++++ packages/frontend/i18n/src/resources/en.json | 4 ++ .../e2e/settings/embedding.spec.ts | 18 +++++ 9 files changed, 225 insertions(+), 5 deletions(-) create mode 100644 packages/frontend/core/src/modules/workspace-indexer-embedding/view/embedding-progress.tsx 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,