diff --git a/packages/common/infra/src/modules/feature-flag/constant.ts b/packages/common/infra/src/modules/feature-flag/constant.ts index 8e5e2377cf..92c2b8d336 100644 --- a/packages/common/infra/src/modules/feature-flag/constant.ts +++ b/packages/common/infra/src/modules/feature-flag/constant.ts @@ -5,6 +5,14 @@ const isDesktopEnvironment = environment.isElectron; const isCanaryBuild = runtimeConfig.appBuildType === 'canary'; export const AFFINE_FLAGS = { + enable_ai: { + category: 'affine', + displayName: 'Enable AI', + description: 'Enable or disable ALL AI features.', + hide: true, + configurable: true, + defaultState: true, + }, enable_database_attachment_note: { category: 'blocksuite', bsFlag: 'enable_database_attachment_note', diff --git a/packages/common/infra/src/modules/feature-flag/entities/flags.ts b/packages/common/infra/src/modules/feature-flag/entities/flags.ts index 340e68dbf8..b21f1c58ac 100644 --- a/packages/common/infra/src/modules/feature-flag/entities/flags.ts +++ b/packages/common/infra/src/modules/feature-flag/entities/flags.ts @@ -29,12 +29,17 @@ export class Flags extends Entity { const configurable = flag.configurable ?? true; const defaultState = 'defaultState' in flag ? flag.defaultState : undefined; + const getValue = () => { + return configurable + ? (this.globalState.get(FLAG_PREFIX + flagKey) ?? + defaultState) + : defaultState; + }; const item = { ...flag, - value: configurable - ? (this.globalState.get(FLAG_PREFIX + flagKey) ?? - defaultState) - : defaultState, + get value() { + return getValue(); + }, set: (value: boolean) => { if (!configurable) { return; diff --git a/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts b/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts index f5b356a6e1..efe1fbc84e 100644 --- a/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts +++ b/packages/common/infra/src/modules/feature-flag/services/feature-flag.ts @@ -1,10 +1,14 @@ +import { distinctUntilChanged, skip } from 'rxjs'; + import { OnEvent, Service } from '../../../framework'; +import { ApplicationStarted } from '../../lifecycle'; import type { Workspace } from '../../workspace'; import { WorkspaceInitialized } from '../../workspace/events'; import { AFFINE_FLAGS } from '../constant'; import { Flags, type FlagsExt } from '../entities/flags'; @OnEvent(WorkspaceInitialized, e => e.setupBlocksuiteEditorFlags) +@OnEvent(ApplicationStarted, e => e.setupRestartListener) export class FeatureFlagService extends Service { flags = this.framework.createEntity(Flags) as FlagsExt; @@ -18,4 +22,13 @@ export class FeatureFlagService extends Service { } } } + + setupRestartListener() { + this.flags.enable_ai.$.pipe(distinctUntilChanged(), skip(1)).subscribe( + () => { + // when enable_ai flag changes, reload the page. + window.location.reload(); + } + ); + } } diff --git a/packages/common/infra/src/modules/feature-flag/types.ts b/packages/common/infra/src/modules/feature-flag/types.ts index c521df716c..e01209276b 100644 --- a/packages/common/infra/src/modules/feature-flag/types.ts +++ b/packages/common/infra/src/modules/feature-flag/types.ts @@ -5,6 +5,10 @@ export type FlagInfo = { description?: string; configurable?: boolean; defaultState?: boolean; // default to open and not controlled by user + /** + * hide in the feature flag settings, but still can be controlled by the code + */ + hide?: boolean; feedbackType?: FeedbackType; feedbackLink?: string; } & ( diff --git a/packages/frontend/core/src/components/affine/ai-onboarding/index.tsx b/packages/frontend/core/src/components/affine/ai-onboarding/index.tsx index 429d9acb90..5c94350c87 100644 --- a/packages/frontend/core/src/components/affine/ai-onboarding/index.tsx +++ b/packages/frontend/core/src/components/affine/ai-onboarding/index.tsx @@ -1,5 +1,4 @@ -import { EditorSettingService } from '@affine/core/modules/editor-settting'; -import { useLiveData, useService } from '@toeverything/infra'; +import { FeatureFlagService, useService } from '@toeverything/infra'; import { Suspense, useCallback, useEffect, useState } from 'react'; import { AIOnboardingEdgeless } from './edgeless.dialog'; @@ -30,10 +29,8 @@ const useDismiss = (key: AIOnboardingType) => { export const WorkspaceAIOnboarding = () => { const [dismissGeneral] = useDismiss(AIOnboardingType.GENERAL); const [dismissLocal] = useDismiss(AIOnboardingType.LOCAL); - const editorSettingService = useService(EditorSettingService); - const enableAI = useLiveData( - editorSettingService.editorSetting.settings$.map(s => s.enableAI) - ); + const featureFlagService = useService(FeatureFlagService); + const enableAI = featureFlagService.flags.enable_ai.value; return ( @@ -45,10 +42,8 @@ export const WorkspaceAIOnboarding = () => { export const PageAIOnboarding = () => { const [dismissEdgeless] = useDismiss(AIOnboardingType.EDGELESS); - const editorSettingService = useService(EditorSettingService); - const enableAI = useLiveData( - editorSettingService.editorSetting.settings$.map(s => s.enableAI) - ); + const featureFlagService = useService(FeatureFlagService); + const enableAI = featureFlagService.flags.enable_ai.value; return ( diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx index 323e4a83f8..88fecdf40b 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/editor/general.tsx @@ -26,7 +26,11 @@ import { import { useI18n } from '@affine/i18n'; import type { DocMode } from '@blocksuite/blocks'; import { DoneIcon, SearchIcon } from '@blocksuite/icons/rc'; -import { useLiveData, useServices } from '@toeverything/infra'; +import { + FeatureFlagService, + useLiveData, + useServices, +} from '@toeverything/infra'; import clsx from 'clsx'; import { type ChangeEvent, @@ -398,15 +402,15 @@ export const SpellCheckSettings = () => { const AISettings = () => { const t = useI18n(); const { openConfirmModal } = useConfirmModal(); - const { editorSettingService } = useServices({ EditorSettingService }); + const { featureFlagService } = useServices({ FeatureFlagService }); - const settings = useLiveData(editorSettingService.editorSetting.settings$); + const enableAI = useLiveData(featureFlagService.flags.enable_ai.$); const onAIChange = useCallback( (checked: boolean) => { - editorSettingService.editorSetting.set('enableAI', checked); + featureFlagService.flags.enable_ai.set(checked); // this will trigger page reload, see `FeatureFlagService` }, - [editorSettingService] + [featureFlagService] ); const onToggleAI = useCallback( (checked: boolean) => { @@ -421,7 +425,11 @@ const AISettings = () => { : t[ 'com.affine.settings.editorSettings.general.ai.disable.description' ](), - confirmText: checked ? t['Enable']() : t['Disable'](), + confirmText: checked + ? t['com.affine.settings.editorSettings.general.ai.enable.confirm']() + : t[ + 'com.affine.settings.editorSettings.general.ai.disable.confirm' + ](), cancelText: t['Cancel'](), onConfirm: () => onAIChange(checked), confirmButtonOptions: { @@ -437,7 +445,7 @@ const AISettings = () => { name={t['com.affine.settings.editorSettings.general.ai.title']()} desc={t['com.affine.settings.editorSettings.general.ai.description']()} > - + ); }; diff --git a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx index d92495737d..5a1859c436 100644 --- a/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx +++ b/packages/frontend/core/src/components/affine/setting-modal/general-setting/experimental-features/index.tsx @@ -120,7 +120,7 @@ const ExperimentalFeaturesItem = ({ flag }: { flag: Flag }) => { : feedbackLink[flag.feedbackType] : undefined; - if (flag.configurable === false) { + if (flag.configurable === false || flag.hide) { return null; } diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx index f10b2f5d52..fe7eba6774 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/lit-adaper.tsx @@ -13,6 +13,7 @@ import type { Doc } from '@blocksuite/store'; import { DocService, DocsService, + FeatureFlagService, useFramework, useLiveData, useService, @@ -71,14 +72,14 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => { peekViewService, docService, docsService, - editorSettingService, editorService, + featureFlagService, } = useServices({ PeekViewService, DocService, DocsService, - EditorSettingService, EditorService, + FeatureFlagService, }); const framework = useFramework(); const referenceRenderer: ReferenceReactRenderer = useMemo(() => { @@ -101,12 +102,11 @@ const usePatchSpecs = (page: Doc, shared: boolean, mode: DocMode) => { }, [mode, page.collection]); const specs = useMemo(() => { - const enableAI = - editorSettingService.editorSetting.settings$.value.enableAI; + const enableAI = featureFlagService.flags.enable_ai.value; return mode === 'edgeless' ? createEdgelessModeSpecs(framework, enableAI) : createPageModeSpecs(framework, enableAI); - }, [editorSettingService, mode, framework]); + }, [featureFlagService, mode, framework]); const confirmModal = useConfirmModal(); const patchedSpecs = useMemo(() => { diff --git a/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts b/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts index c0f5c1f145..b8966a0148 100644 --- a/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts +++ b/packages/frontend/core/src/modules/editor-settting/entities/editor-setting.ts @@ -11,6 +11,13 @@ import { map } from 'rxjs'; import type { EditorSettingProvider } from '../provider/editor-setting-provider'; import { EditorSettingSchema } from '../schema'; +type SettingItem = { + readonly value: T; + set: (value: T) => void; + // eslint-disable-next-line rxjs/finnish + $: T; +}; + export class EditorSetting extends Entity { constructor(public readonly provider: EditorSettingProvider) { super(); @@ -20,6 +27,27 @@ export class EditorSetting extends Entity { >(this.settings$, {}); this.settingSignal = signal; this.disposables.push(cleanup); + + Object.entries(EditorSettingSchema.shape).forEach(([flagKey, flag]) => { + const livedata$ = this.settings$.selector( + s => s[flagKey as keyof EditorSettingSchema] + ); + const item = { + ...flag, + get value() { + return livedata$.value; + }, + set: (value: any) => { + this.set(flagKey as keyof EditorSettingSchema, value); + }, + $: livedata$, + } as SettingItem; + Object.defineProperty(this, flagKey, { + get: () => { + return item; + }, + }); + }); } settings$ = LiveData.from(this.watchAll(), null as any); @@ -61,3 +89,7 @@ export class EditorSetting extends Entity { ); } } + +export type EditorSettingExt = EditorSetting & { + [K in keyof EditorSettingSchema]: SettingItem; +}; diff --git a/packages/frontend/core/src/modules/editor-settting/schema.ts b/packages/frontend/core/src/modules/editor-settting/schema.ts index d79c45a3be..cfb256a2aa 100644 --- a/packages/frontend/core/src/modules/editor-settting/schema.ts +++ b/packages/frontend/core/src/modules/editor-settting/schema.ts @@ -16,7 +16,6 @@ export const fontStyleOptions = [ }[]; const AffineEditorSettingSchema = z.object({ - enableAI: z.boolean().default(true), fontFamily: z.enum(['Sans', 'Serif', 'Mono', 'Custom']).default('Sans'), customFontFamily: z.string().default(''), newDocDefaultMode: z.enum(['edgeless', 'page']).default('page'), diff --git a/packages/frontend/core/src/modules/editor-settting/services/editor-setting.ts b/packages/frontend/core/src/modules/editor-settting/services/editor-setting.ts index 7815c582c0..b13b42f00c 100644 --- a/packages/frontend/core/src/modules/editor-settting/services/editor-setting.ts +++ b/packages/frontend/core/src/modules/editor-settting/services/editor-setting.ts @@ -6,11 +6,16 @@ import { WorkspaceInitialized, } from '@toeverything/infra'; -import { EditorSetting } from '../entities/editor-setting'; +import { + EditorSetting, + type EditorSettingExt, +} from '../entities/editor-setting'; @OnEvent(WorkspaceInitialized, e => e.onWorkspaceInitialized) export class EditorSettingService extends Service { - editorSetting = this.framework.createEntity(EditorSetting); + editorSetting = this.framework.createEntity( + EditorSetting + ) as EditorSettingExt; onWorkspaceInitialized(workspace: Workspace) { // set default mode for new doc diff --git a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx index 094b868615..a7cb31740e 100644 --- a/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/pages/workspace/detail-page/detail-page.tsx @@ -7,7 +7,6 @@ import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline- import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper'; import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta'; import { EditorService } from '@affine/core/modules/editor'; -import { EditorSettingService } from '@affine/core/modules/editor-settting'; import { RecentDocsService } from '@affine/core/modules/quicksearch'; import { ViewService } from '@affine/core/modules/workbench/services/view'; import type { PageRootService } from '@blocksuite/blocks'; @@ -16,6 +15,7 @@ import { AiIcon, FrameIcon, TocIcon, TodayIcon } from '@blocksuite/icons/rc'; import { type AffineEditorContainer } from '@blocksuite/presets'; import { DocService, + FeatureFlagService, FrameworkScope, GlobalContextService, useLiveData, @@ -61,7 +61,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { docService, workspaceService, globalContextService, - editorSettingService, + featureFlagService, } = useServices({ WorkbenchService, ViewService, @@ -69,7 +69,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { DocService, WorkspaceService, GlobalContextService, - EditorSettingService, + FeatureFlagService, }); const workbench = workbenchService.workbench; const editor = editorService.editor; @@ -250,7 +250,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { - {editorSettingService.editorSetting.settings$.value.enableAI && ( + {featureFlagService.flags.enable_ai.value && ( } diff --git a/packages/frontend/core/src/providers/modal-provider.tsx b/packages/frontend/core/src/providers/modal-provider.tsx index a815401a0e..9de670958b 100644 --- a/packages/frontend/core/src/providers/modal-provider.tsx +++ b/packages/frontend/core/src/providers/modal-provider.tsx @@ -1,7 +1,6 @@ -import { ConfirmModal, NotificationCenter, notify } from '@affine/component'; +import { NotificationCenter, notify } from '@affine/component'; import { events } from '@affine/electron-api'; import { WorkspaceFlavour } from '@affine/env/workspace'; -import { useI18n } from '@affine/i18n'; import { GlobalContextService, useLiveData, @@ -11,7 +10,7 @@ import { } from '@toeverything/infra'; import { useAtom } from 'jotai'; import type { ReactElement } from 'react'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect } from 'react'; import type { SettingAtom } from '../atoms'; import { openSettingModalAtom, openSignOutModalAtom } from '../atoms'; @@ -32,7 +31,6 @@ import { useAsyncCallback } from '../hooks/affine-async-hooks'; import { useNavigateHelper } from '../hooks/use-navigate-helper'; import { AuthService } from '../modules/cloud/services/auth'; import { CreateWorkspaceDialogProvider } from '../modules/create-workspace'; -import { EditorSettingService } from '../modules/editor-settting'; import { FindInPageModal } from '../modules/find-in-page/view/find-in-page-modal'; import { ImportTemplateDialogProvider } from '../modules/import-template'; import { PeekViewManagerModal } from '../modules/peek-view'; @@ -185,43 +183,6 @@ export const SignOutConfirmModal = () => { ); }; -export const AIReloadConfirmModal = () => { - const t = useI18n(); - const editorSettingService = useService(EditorSettingService); - const enableAI = useLiveData( - editorSettingService.editorSetting.settings$.selector(s => s.enableAI) - ); - const [aiState] = useState(enableAI); - const [open, setOpen] = useState(false); - - useEffect(() => { - setOpen(enableAI !== aiState); - }, [aiState, enableAI]); - - const onConfirm = useCallback(() => { - window.location.reload(); - }, []); - - return ( - - ); -}; - export const AllWorkspaceModals = (): ReactElement => { return ( <> @@ -230,7 +191,6 @@ export const AllWorkspaceModals = (): ReactElement => { - ); }; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index a754b821ff..47971b83fc 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1312,8 +1312,10 @@ "com.affine.settings.editorSettings.general.ai.description": "Enable the powerful AI assistant, AFFiNE AI.", "com.affine.settings.editorSettings.general.ai.disable.description": "Are you sure you want to disable AI? We value your productivity and our AI can enhance it. Please think again!", "com.affine.settings.editorSettings.general.ai.disable.title": "Disable AI?", - "com.affine.settings.editorSettings.general.ai.enable.description": "Do you want to enable AI? Our AI assistant is ready to enhance your productivity and provide smart assistance. Let's get started!", + "com.affine.settings.editorSettings.general.ai.disable.confirm": "Disable AI and Reload", + "com.affine.settings.editorSettings.general.ai.enable.description": "Do you want to enable AI? Our AI assistant is ready to enhance your productivity and provide smart assistance. Let's get started! We need reload page to make this change.", "com.affine.settings.editorSettings.general.ai.enable.title": "Enable AI?", + "com.affine.settings.editorSettings.general.ai.enable.confirm": "Enable AI and Reload", "com.affine.settings.editorSettings.general.ai.reload.confirm": "Reload", "com.affine.settings.editorSettings.general.ai.reload.description": "AI settings have been updated. Please reload the page to apply the changes.", "com.affine.settings.editorSettings.general.ai.reload.title": "You need to reload the page", diff --git a/packages/frontend/i18n/src/resources/zh-Hans.json b/packages/frontend/i18n/src/resources/zh-Hans.json index 21d8004772..19e670bae5 100644 --- a/packages/frontend/i18n/src/resources/zh-Hans.json +++ b/packages/frontend/i18n/src/resources/zh-Hans.json @@ -1313,7 +1313,7 @@ "com.affine.settings.editorSettings.general.ai.description": "启用卓越的 AI 助手,AFFiNE AI。", "com.affine.settings.editorSettings.general.ai.disable.description": "您确定要禁用 AI 吗?我们重视您的工作效率,而我们的 AI 可以提高它。请再考虑一下!", "com.affine.settings.editorSettings.general.ai.disable.title": "禁用 AI ?", - "com.affine.settings.editorSettings.general.ai.enable.description": "您想启用 AI 吗?我们的 AI 助手已准备好提高您的工作效率并提供智能帮助。让我们开始吧!", + "com.affine.settings.editorSettings.general.ai.enable.description": "您想启用 AI 吗?我们的 AI 助手已准备好提高您的工作效率并提供智能帮助。让我们开始吧!我们需要重新加载页面以进行此更改。", "com.affine.settings.editorSettings.general.ai.enable.title": "启用 AI ?", "com.affine.settings.editorSettings.general.ai.reload.confirm": "重新加载", "com.affine.settings.editorSettings.general.ai.reload.description": "AI 设置已更新。请重新加载页面以应用更改。",