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


> 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',
});
}