From 66db63c845181622307a8aafb7d02d4a9636c337 Mon Sep 17 00:00:00 2001 From: yoyoyohamapi <8338436+yoyoyohamapi@users.noreply.github.com> Date: Thu, 29 May 2025 04:33:07 +0000 Subject: [PATCH] feat(core): no-access & local for workspace embedding (#12598) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## TL;DR Workspace embedding settings opt: * **local workspace**: show enable cloud panel * **no-access workspace**: disable settings panel ![截屏2025-05-28 14.59.36.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/58e1f511-9bde-487e-a4cd-f0818c582fcb.png) ![截屏2025-05-28 15.00.19.png](https://graphite-user-uploaded-assets-prod.s3.amazonaws.com/MyktQ6Qwc7H6TiRCFoYN/25c3db30-bf31-4c92-a771-55ce871c9a7a.png) > CLOSE AI-155 ## Summary by CodeRabbit - **New Features** - Embedding settings UI now displays a tooltip indicating that only workspace owners can enable Workspace Embedding. - Embedding settings are modularized for local and cloud workspaces, with clear separation and appropriate enablement controls. - Attachments in embedding settings cannot be deleted when the settings are disabled. - **Accessibility** - Settings wrapper now includes an aria-disabled attribute for improved assistive technology support. - **Localization** - Added a new tooltip message: "Only the workspace owner can enable Workspace Embedding." - **Tests** - Added end-to-end tests for local workspace UI and disabled state when not the workspace owner. - **UI Improvements** - Updated settings panel to better reflect disabled states with tooltips and conditional controls. - Improved synchronization when opening the embedding settings panel for a smoother user experience. --- .../components/setting-components/wrapper.tsx | 1 + .../view/attachments.tsx | 35 +-- .../view/embedding-settings.tsx | 207 +++++++++++------- packages/frontend/i18n/src/i18n.gen.ts | 4 + packages/frontend/i18n/src/resources/en.json | 1 + .../e2e/settings/embedding.spec.ts | 52 +++++ .../e2e/utils/settings-panel-utils.ts | 2 +- 7 files changed, 207 insertions(+), 95 deletions(-) diff --git a/packages/frontend/component/src/components/setting-components/wrapper.tsx b/packages/frontend/component/src/components/setting-components/wrapper.tsx index cfdd5bd36..4aec5c3e3 100644 --- a/packages/frontend/component/src/components/setting-components/wrapper.tsx +++ b/packages/frontend/component/src/components/setting-components/wrapper.tsx @@ -22,6 +22,7 @@ export const SettingWrapper = ({ id={id} className={clsx(wrapper, disabled && wrapperDisabled)} data-testid={testId} + aria-disabled={disabled} > {title ?
{title}
: null} {children} diff --git a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/attachments.tsx b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/attachments.tsx index b8f6e2df9..d1103e5e6 100644 --- a/packages/frontend/core/src/modules/workspace-indexer-embedding/view/attachments.tsx +++ b/packages/frontend/core/src/modules/workspace-indexer-embedding/view/attachments.tsx @@ -33,11 +33,13 @@ interface AttachmentsProps { totalCount: number; onPageChange: (offset: number) => void; onDelete: (id: string) => void; + disabled: boolean; } interface AttachmentItemProps { attachment: AttachmentFile; onDelete: (id: string) => void; + disabled: boolean; } const UploadingItem: React.FC<{ attachment: UploadingAttachmentFile }> = ({ @@ -88,6 +90,7 @@ const PersistedItem: React.FC<{ attachment: PersistedAttachmentFile }> = ({ const AttachmentItem: React.FC = ({ attachment, onDelete, + disabled, }) => { const t = useI18n(); const { openConfirmModal } = useConfirmModal(); @@ -131,20 +134,22 @@ const AttachmentItem: React.FC = ({ ) : isPersistedAttachment(attachment) ? ( ) : null} -
- - - -
+ {!disabled && ( +
+ + + +
+ )} ); }; @@ -154,6 +159,7 @@ export const Attachments: React.FC = ({ totalCount, onDelete, onPageChange, + disabled, }) => { const handlePageChange = useCallback( (offset: number) => { @@ -172,6 +178,7 @@ export const Attachments: React.FC = ({ key={getAttachmentId(attachment)} attachment={attachment} onDelete={onDelete} + disabled={disabled} /> ))} = () => { +const EmbeddingLocal: React.FC<{}> = () => { + return ; +}; + +const EmbeddingCloud: React.FC<{ disabled: boolean }> = ({ disabled }) => { const t = useI18n(); const embeddingService = useService(EmbeddingService); const workspaceDialogService = useService(WorkspaceDialogService); @@ -171,96 +179,135 @@ export const EmbeddingSettings: React.FC = () => { ]); return ( - <> - + - - - - - - + - - - - - - - - - - {attachments.length > 0 && ( - + { + <> + - )} - + + + } + + + - + + - {ignoredDocs.length > 0 && ( - - )} - + {attachments.length > 0 && ( + + )} + + + + + + {ignoredDocs.length > 0 && ( + + )} + + ); +}; + +export const EmbeddingSettings: React.FC = () => { + const workspaceService = useService(WorkspaceService); + const serverService = useService(ServerService); + const isLocal = workspaceService.workspace.flavour === 'local'; + const serverConfig = useLiveData(serverService.server.config$); + const isEmbeddingEnabled = serverConfig?.features.includes( + ServerFeature.CopilotEmbedding + ); + + const t = useI18n(); + + return ( + <> + + + + + {isLocal ? ( + + ) : ( + + )} ); }; diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index e1543bdee..c93839740 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -6285,6 +6285,10 @@ export function useAFFiNEI18N(): { * `Embedding allows AI to retrieve your content. If the indexer uses local settings, it may affect some of the results of the Embedding.` */ ["com.affine.settings.workspace.indexer-embedding.embedding.description"](): string; + /** + * `Only the workspace owner can enable Workspace Embedding.` + */ + ["com.affine.settings.workspace.indexer-embedding.embedding.disabled-tooltip"](): string; /** * `Select doc` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 18ff77036..4ac942a9d 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1575,6 +1575,7 @@ "com.affine.settings.workspace.indexer-embedding.description": "Manage AFFiNE indexing and AFFiNE AI Embedding for local content processing", "com.affine.settings.workspace.indexer-embedding.embedding.title": "Embedding", "com.affine.settings.workspace.indexer-embedding.embedding.description": "Embedding allows AI to retrieve your content. If the indexer uses local settings, it may affect some of the results of the Embedding.", + "com.affine.settings.workspace.indexer-embedding.embedding.disabled-tooltip": "Only the workspace owner can enable Workspace Embedding.", "com.affine.settings.workspace.indexer-embedding.embedding.select-doc": "Select doc", "com.affine.settings.workspace.indexer-embedding.embedding.upload-file": "Upload file", "com.affine.settings.workspace.indexer-embedding.embedding.switch.title": "Workspace Embedding", diff --git a/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts b/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts index ee13f8bfe..52db8a8b2 100644 --- a/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts +++ b/tests/affine-cloud-copilot/e2e/settings/embedding.spec.ts @@ -1,3 +1,4 @@ +import { createLocalWorkspace } from '@affine-test/kit/utils/workspace'; import { expect } from '@playwright/test'; import { test } from '../base/base-test'; @@ -43,6 +44,57 @@ test.describe('AISettings/Embedding', () => { await utils.settings.waitForWorkspaceEmbeddingSwitchToBe(page, true); }); + test('should show enable cloud panel if workspace is local', async ({ + loggedInPage: page, + utils, + }) => { + await utils.settings.closeSettingsPanel(page); + await createLocalWorkspace({ name: 'test' }, page); + await utils.settings.openSettingsPanel(page); + await expect( + page.getByTestId('publish-enable-affine-cloud-button') + ).toBeVisible(); + }); + + test('should disable embedding settings if the user is not workspace owner', async ({ + loggedInPage: page, + utils, + }) => { + // mock the features to be empty(without CopilotEmbedding) + await page.route('**/graphql', async (route, request) => { + const postData = request.postData(); + if (postData && postData.includes('serverConfig')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + data: { + serverConfig: { + version: '1.0.0', + baseUrl: 'http://localhost:8080', + name: 'AFFiNE', + features: [], + type: 'cloud', + initialized: true, + credentialsRequirement: null, + }, + }, + }), + }); + } else { + await route.continue(); + } + }); + + await page.reload(); + await utils.settings.openSettingsPanel(page); + + const wrapper = await page.getByTestId( + 'workspace-embedding-setting-wrapper' + ); + await expect(wrapper).toHaveAttribute('aria-disabled', 'true'); + }); + test('should show error message if enable workspace embedding failed', async ({ loggedInPage: page, utils, diff --git a/tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts b/tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts index ac46288ce..b59934028 100644 --- a/tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts +++ b/tests/affine-cloud-copilot/e2e/utils/settings-panel-utils.ts @@ -7,7 +7,7 @@ export class SettingsPanelUtils { if (await page.getByTestId('workspace-setting:embedding').isHidden()) { await page.getByTestId('slider-bar-workspace-setting-button').click(); await page.getByTestId('workspace-setting:embedding').click(); - await page.getByTestId('workspace-embedding-setting-wrapper').waitFor({ + await page.getByTestId('workspace-embedding-setting-header').waitFor({ state: 'visible', }); }