From 9435118ef1fe2a6691f01af4ff6ee3eb6be89a0c Mon Sep 17 00:00:00 2001 From: Saul-Mirone Date: Mon, 24 Feb 2025 04:30:08 +0000 Subject: [PATCH] refactor(editor): optimize ai code structure (#10381) Let me analyze this diff and provide a clear description of the changes. This PR introduces several significant changes focused on AI integration and code organization in the AFFiNE codebase: 1. **Enhanced SpecBuilder Functionality** (`blocksuite/affine/shared/src/utils/spec/spec-builder.ts`): - Added method chaining by returning `this` from `extend`, `omit`, and `replace` methods - Added new utility methods: - `hasAll(target: ExtensionType[])`: Checks if all specified extensions exist - `hasOneOf(target: ExtensionType[])`: Checks if at least one specified extension exists 2. **AI Extensions Modularization**: - Split the large AI-related code into separate modular files under `packages/frontend/core/src/blocksuite/ai/extensions/`: - `ai-code.ts`: Code block AI integration - `ai-edgeless-root.ts`: Edgeless mode AI features - `ai-image.ts`: Image block AI capabilities - `ai-page-root.ts`: Page root AI integration - `ai-paragraph.ts`: Paragraph block AI features - `enable-ai.ts`: Central AI extension enablement logic 3. **Widget Improvements**: - Enhanced `AffineAIPanelWidget` and `EdgelessCopilotWidget` with proper widget extensions - Moved widget-specific extensions into their respective files - Added proper type definitions and component registrations 4. **Code Organization**: - Simplified exports in `index.ts` - Better separation of concerns between different AI-related components - More modular approach to AI feature integration 5. **AI Integration Architecture**: - Introduced a new `enableAIExtension` function that handles: - Replacing standard blocks with AI-enhanced versions - Conditional enabling of AI features based on the current spec configuration - Extension of AI chat capabilities The changes primarily focus on improving code organization, maintainability, and the architecture of AI feature integration in the AFFiNE editor. The modularization will make it easier to maintain and extend AI capabilities across different block types and editor modes. --- .../block-root/src/common-specs/index.ts | 6 +- .../block-root/src/common-specs/widgets.ts | 12 -- .../src/edgeless/edgeless-root-spec.ts | 21 +- .../shared/src/utils/spec/spec-builder.ts | 11 + .../affine/widget-drag-handle/src/index.ts | 11 + .../widget-edgeless-auto-connect/src/index.ts | 9 +- .../affine/widget-frame-title/src/index.ts | 9 +- .../widget-remote-selection/src/index.ts | 18 +- .../widget-scroll-anchoring/src/index.ts | 11 + .../core/src/blocksuite/ai/ai-spec.ts | 199 ------------------ .../src/blocksuite/ai/extensions/ai-code.ts | 27 +++ .../ai/extensions/ai-edgeless-root.ts | 73 +++++++ .../src/blocksuite/ai/extensions/ai-image.ts | 26 +++ .../blocksuite/ai/extensions/ai-page-root.ts | 49 +++++ .../blocksuite/ai/extensions/ai-paragraph.ts | 38 ++++ .../src/blocksuite/ai/extensions/enable-ai.ts | 37 ++++ .../src/blocksuite/ai/extensions/index.ts | 1 + .../frontend/core/src/blocksuite/ai/index.ts | 10 +- .../ai/widgets/ai-panel/ai-panel.ts | 12 +- .../ai/widgets/edgeless-copilot/index.ts | 12 +- .../block-suite-editor/specs/common.ts | 60 ------ .../specs/custom/root-block.ts | 16 -- .../block-suite-editor/specs/edgeless.ts | 16 +- .../block-suite-editor/specs/page.ts | 13 +- 24 files changed, 357 insertions(+), 340 deletions(-) delete mode 100644 packages/frontend/core/src/blocksuite/ai/ai-spec.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/ai-image.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/ai-paragraph.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts create mode 100644 packages/frontend/core/src/blocksuite/ai/extensions/index.ts delete mode 100644 packages/frontend/core/src/blocksuite/block-suite-editor/specs/common.ts diff --git a/blocksuite/affine/block-root/src/common-specs/index.ts b/blocksuite/affine/block-root/src/common-specs/index.ts index 9396f36763..1cc89358d7 100644 --- a/blocksuite/affine/block-root/src/common-specs/index.ts +++ b/blocksuite/affine/block-root/src/common-specs/index.ts @@ -6,19 +6,19 @@ import { PageViewportServiceExtension, ThemeService, } from '@blocksuite/affine-shared/services'; +import { dragHandleWidget } from '@blocksuite/affine-widget-drag-handle'; +import { docRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection'; +import { scrollAnchoringWidget } from '@blocksuite/affine-widget-scroll-anchoring'; import { FlavourExtension } from '@blocksuite/block-std'; import type { ExtensionType } from '@blocksuite/store'; import { RootBlockAdapterExtensions } from '../adapters/extension'; import { - docRemoteSelectionWidget, - dragHandleWidget, embedCardToolbarWidget, formatBarWidget, innerModalWidget, linkedDocWidget, modalWidget, - scrollAnchoringWidget, slashMenuWidget, viewportOverlayWidget, } from './widgets'; diff --git a/blocksuite/affine/block-root/src/common-specs/widgets.ts b/blocksuite/affine/block-root/src/common-specs/widgets.ts index b6076d1cdf..ecb6207427 100644 --- a/blocksuite/affine/block-root/src/common-specs/widgets.ts +++ b/blocksuite/affine/block-root/src/common-specs/widgets.ts @@ -1,5 +1,3 @@ -import { AFFINE_DRAG_HANDLE_WIDGET } from '@blocksuite/affine-widget-drag-handle'; -import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection'; import { AFFINE_SCROLL_ANCHORING_WIDGET } from '@blocksuite/affine-widget-scroll-anchoring'; import { WidgetViewExtension } from '@blocksuite/block-std'; import { literal, unsafeStatic } from 'lit/static-html.js'; @@ -32,11 +30,6 @@ export const linkedDocWidget = WidgetViewExtension( AFFINE_LINKED_DOC_WIDGET, literal`${unsafeStatic(AFFINE_LINKED_DOC_WIDGET)}` ); -export const dragHandleWidget = WidgetViewExtension( - 'affine:page', - AFFINE_DRAG_HANDLE_WIDGET, - literal`${unsafeStatic(AFFINE_DRAG_HANDLE_WIDGET)}` -); export const embedCardToolbarWidget = WidgetViewExtension( 'affine:page', AFFINE_EMBED_CARD_TOOLBAR_WIDGET, @@ -47,11 +40,6 @@ export const formatBarWidget = WidgetViewExtension( AFFINE_FORMAT_BAR_WIDGET, literal`${unsafeStatic(AFFINE_FORMAT_BAR_WIDGET)}` ); -export const docRemoteSelectionWidget = WidgetViewExtension( - 'affine:page', - AFFINE_DOC_REMOTE_SELECTION_WIDGET, - literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}` -); export const viewportOverlayWidget = WidgetViewExtension( 'affine:page', AFFINE_VIEWPORT_OVERLAY_WIDGET, diff --git a/blocksuite/affine/block-root/src/edgeless/edgeless-root-spec.ts b/blocksuite/affine/block-root/src/edgeless/edgeless-root-spec.ts index dd4502bde6..3e7c5607ff 100644 --- a/blocksuite/affine/block-root/src/edgeless/edgeless-root-spec.ts +++ b/blocksuite/affine/block-root/src/edgeless/edgeless-root-spec.ts @@ -1,6 +1,6 @@ -import { AFFINE_EDGELESS_AUTO_CONNECT_WIDGET } from '@blocksuite/affine-widget-edgeless-auto-connect'; -import { AFFINE_FRAME_TITLE_WIDGET } from '@blocksuite/affine-widget-frame-title'; -import { AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET } from '@blocksuite/affine-widget-remote-selection'; +import { autoConnectWidget } from '@blocksuite/affine-widget-edgeless-auto-connect'; +import { frameTitleWidget } from '@blocksuite/affine-widget-frame-title'; +import { edgelessRemoteSelectionWidget } from '@blocksuite/affine-widget-remote-selection'; import { BlockServiceWatcher, BlockViewExtension, @@ -20,31 +20,16 @@ import { EDGELESS_SELECTED_RECT_WIDGET } from './components/rects/edgeless-selec import { EDGELESS_TOOLBAR_WIDGET } from './components/toolbar/edgeless-toolbar.js'; import { EdgelessRootService } from './edgeless-root-service.js'; -export const edgelessRemoteSelectionWidget = WidgetViewExtension( - 'affine:page', - AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET, - literal`${unsafeStatic(AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET)}` -); export const edgelessZoomToolbarWidget = WidgetViewExtension( 'affine:page', AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET, literal`${unsafeStatic(AFFINE_EDGELESS_ZOOM_TOOLBAR_WIDGET)}` ); -export const frameTitleWidget = WidgetViewExtension( - 'affine:page', - AFFINE_FRAME_TITLE_WIDGET, - literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}` -); export const elementToolbarWidget = WidgetViewExtension( 'affine:page', EDGELESS_ELEMENT_TOOLBAR_WIDGET, literal`${unsafeStatic(EDGELESS_ELEMENT_TOOLBAR_WIDGET)}` ); -export const autoConnectWidget = WidgetViewExtension( - 'affine:page', - AFFINE_EDGELESS_AUTO_CONNECT_WIDGET, - literal`${unsafeStatic(AFFINE_EDGELESS_AUTO_CONNECT_WIDGET)}` -); export const edgelessDraggingAreaWidget = WidgetViewExtension( 'affine:page', EDGELESS_DRAGGING_AREA_WIDGET, diff --git a/blocksuite/affine/shared/src/utils/spec/spec-builder.ts b/blocksuite/affine/shared/src/utils/spec/spec-builder.ts index bbe86d65c8..4dd277341e 100644 --- a/blocksuite/affine/shared/src/utils/spec/spec-builder.ts +++ b/blocksuite/affine/shared/src/utils/spec/spec-builder.ts @@ -13,10 +13,20 @@ export class SpecBuilder { extend(extensions: ExtensionType[]) { this._value = [...this._value, ...extensions]; + return this; } omit(target: ExtensionType) { this._value = this._value.filter(extension => extension !== target); + return this; + } + + hasAll(target: ExtensionType[]) { + return target.every(t => this._value.includes(t)); + } + + hasOneOf(target: ExtensionType[]) { + return target.some(t => this._value.includes(t)); } replace(target: ExtensionType[], newExtension: ExtensionType[]) { @@ -24,5 +34,6 @@ export class SpecBuilder { ...this._value.filter(extension => !target.includes(extension)), ...newExtension, ]; + return this; } } diff --git a/blocksuite/affine/widget-drag-handle/src/index.ts b/blocksuite/affine/widget-drag-handle/src/index.ts index 1703f9557f..e6882a6849 100644 --- a/blocksuite/affine/widget-drag-handle/src/index.ts +++ b/blocksuite/affine/widget-drag-handle/src/index.ts @@ -1,4 +1,15 @@ +import { WidgetViewExtension } from '@blocksuite/block-std'; +import { literal, unsafeStatic } from 'lit/static-html.js'; + +import { AFFINE_DRAG_HANDLE_WIDGET } from './consts'; + export * from './consts'; export * from './drag-handle'; export * from './utils'; export type { DragBlockPayload } from './watchers/drag-event-watcher'; + +export const dragHandleWidget = WidgetViewExtension( + 'affine:page', + AFFINE_DRAG_HANDLE_WIDGET, + literal`${unsafeStatic(AFFINE_DRAG_HANDLE_WIDGET)}` +); diff --git a/blocksuite/affine/widget-edgeless-auto-connect/src/index.ts b/blocksuite/affine/widget-edgeless-auto-connect/src/index.ts index bcc4322ca4..01b857d8aa 100644 --- a/blocksuite/affine/widget-edgeless-auto-connect/src/index.ts +++ b/blocksuite/affine/widget-edgeless-auto-connect/src/index.ts @@ -13,7 +13,7 @@ import { } from '@blocksuite/affine-model'; import { FeatureFlagService } from '@blocksuite/affine-shared/services'; import { matchModels, stopPropagation } from '@blocksuite/affine-shared/utils'; -import { WidgetComponent } from '@blocksuite/block-std'; +import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std'; import { type GfxController, GfxControllerIdentifier, @@ -28,6 +28,7 @@ import { css, html, nothing, type TemplateResult } from 'lit'; import { state } from 'lit/decorators.js'; import { repeat } from 'lit/directives/repeat.js'; import { styleMap } from 'lit/directives/style-map.js'; +import { literal, unsafeStatic } from 'lit/static-html.js'; const PAGE_VISIBLE_INDEX_LABEL_WIDTH = 44; const PAGE_VISIBLE_INDEX_LABEL_HEIGHT = 24; @@ -613,6 +614,12 @@ export class EdgelessAutoConnectWidget extends WidgetComponent { private accessor _show = false; } +export const autoConnectWidget = WidgetViewExtension( + 'affine:page', + AFFINE_EDGELESS_AUTO_CONNECT_WIDGET, + literal`${unsafeStatic(AFFINE_EDGELESS_AUTO_CONNECT_WIDGET)}` +); + declare global { interface HTMLElementTagNameMap { 'affine-edgeless-auto-connect-widget': EdgelessAutoConnectWidget; diff --git a/blocksuite/affine/widget-frame-title/src/index.ts b/blocksuite/affine/widget-frame-title/src/index.ts index bd694eafad..1c5f8fd518 100644 --- a/blocksuite/affine/widget-frame-title/src/index.ts +++ b/blocksuite/affine/widget-frame-title/src/index.ts @@ -1,7 +1,8 @@ import { FrameBlockModel, type RootBlockModel } from '@blocksuite/affine-model'; -import { WidgetComponent } from '@blocksuite/block-std'; +import { WidgetComponent, WidgetViewExtension } from '@blocksuite/block-std'; import { html } from 'lit'; import { repeat } from 'lit/directives/repeat.js'; +import { literal, unsafeStatic } from 'lit/static-html.js'; import type { AffineFrameTitle } from './frame-title.js'; @@ -36,3 +37,9 @@ export class AffineFrameTitleWidget extends WidgetComponent { } export * from './styles.js'; + +export const frameTitleWidget = WidgetViewExtension( + 'affine:page', + AFFINE_FRAME_TITLE_WIDGET, + literal`${unsafeStatic(AFFINE_FRAME_TITLE_WIDGET)}` +); diff --git a/blocksuite/affine/widget-remote-selection/src/index.ts b/blocksuite/affine/widget-remote-selection/src/index.ts index 0806b79eaa..92a9b7d6a9 100644 --- a/blocksuite/affine/widget-remote-selection/src/index.ts +++ b/blocksuite/affine/widget-remote-selection/src/index.ts @@ -1,6 +1,20 @@ -import type * as CommandsType from '@blocksuite/affine-shared/commands'; +import { WidgetViewExtension } from '@blocksuite/block-std'; +import { literal, unsafeStatic } from 'lit/static-html.js'; -declare type _GLOBAL_ = typeof CommandsType; +import { AFFINE_DOC_REMOTE_SELECTION_WIDGET } from './doc'; +import { AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET } from './edgeless'; export * from './doc'; export * from './edgeless'; + +export const docRemoteSelectionWidget = WidgetViewExtension( + 'affine:page', + AFFINE_DOC_REMOTE_SELECTION_WIDGET, + literal`${unsafeStatic(AFFINE_DOC_REMOTE_SELECTION_WIDGET)}` +); + +export const edgelessRemoteSelectionWidget = WidgetViewExtension( + 'affine:page', + AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET, + literal`${unsafeStatic(AFFINE_EDGELESS_REMOTE_SELECTION_WIDGET)}` +); diff --git a/blocksuite/affine/widget-scroll-anchoring/src/index.ts b/blocksuite/affine/widget-scroll-anchoring/src/index.ts index 43b770aced..90513a8c78 100644 --- a/blocksuite/affine/widget-scroll-anchoring/src/index.ts +++ b/blocksuite/affine/widget-scroll-anchoring/src/index.ts @@ -1 +1,12 @@ +import { WidgetViewExtension } from '@blocksuite/block-std'; +import { literal, unsafeStatic } from 'lit/static-html.js'; + +import { AFFINE_SCROLL_ANCHORING_WIDGET } from './scroll-anchoring.js'; + export * from './scroll-anchoring.js'; + +export const scrollAnchoringWidget = WidgetViewExtension( + 'affine:page', + AFFINE_SCROLL_ANCHORING_WIDGET, + literal`${unsafeStatic(AFFINE_SCROLL_ANCHORING_WIDGET)}` +); diff --git a/packages/frontend/core/src/blocksuite/ai/ai-spec.ts b/packages/frontend/core/src/blocksuite/ai/ai-spec.ts deleted file mode 100644 index 57cb64f01c..0000000000 --- a/packages/frontend/core/src/blocksuite/ai/ai-spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -import { - BlockServiceWatcher, - WidgetViewExtension, -} from '@blocksuite/affine/block-std'; -import { - AffineCodeToolbarWidget, - AffineFormatBarWidget, - AffineImageToolbarWidget, - AffineSlashMenuWidget, - CodeBlockSpec, - EdgelessElementToolbarWidget, - EdgelessRootBlockSpec, - ImageBlockSpec, - PageRootBlockSpec, - ParagraphBlockService, - ParagraphBlockSpec, -} from '@blocksuite/affine/blocks'; -import { assertInstanceOf } from '@blocksuite/affine/global/utils'; -import type { ExtensionType } from '@blocksuite/affine/store'; -import type { FrameworkProvider } from '@toeverything/infra'; -import { literal, unsafeStatic } from 'lit/static-html.js'; - -import { buildAIPanelConfig } from './ai-panel'; -import { setupCodeToolbarAIEntry } from './entries/code-toolbar/setup-code-toolbar'; -import { - setupEdgelessCopilot, - setupEdgelessElementToolbarAIEntry, -} from './entries/edgeless/index'; -import { setupFormatBarAIEntry } from './entries/format-bar/setup-format-bar'; -import { setupImageToolbarAIEntry } from './entries/image-toolbar/setup-image-toolbar'; -import { setupSlashMenuAIEntry } from './entries/slash-menu/setup-slash-menu'; -import { setupSpaceAIEntry } from './entries/space/setup-space'; -import { CopilotTool } from './tool/copilot-tool'; -import { - AFFINE_AI_PANEL_WIDGET, - AffineAIPanelWidget, -} from './widgets/ai-panel/ai-panel'; -import { - AFFINE_EDGELESS_COPILOT_WIDGET, - EdgelessCopilotWidget, -} from './widgets/edgeless-copilot'; - -function getAIPageRootWatcher(framework: FrameworkProvider) { - class AIPageRootWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:page'; - - override mounted() { - super.mounted(); - this.blockService.specSlots.widgetConnected.on(view => { - if (view.component instanceof AffineAIPanelWidget) { - view.component.style.width = '630px'; - view.component.config = buildAIPanelConfig(view.component, framework); - setupSpaceAIEntry(view.component); - } - - if (view.component instanceof AffineFormatBarWidget) { - setupFormatBarAIEntry(view.component); - } - - if (view.component instanceof AffineSlashMenuWidget) { - setupSlashMenuAIEntry(view.component); - } - }); - } - } - return AIPageRootWatcher; -} - -const aiPanelWidget = WidgetViewExtension( - 'affine:page', - AFFINE_AI_PANEL_WIDGET, - literal`${unsafeStatic(AFFINE_AI_PANEL_WIDGET)}` -); - -const edgelessCopilotWidget = WidgetViewExtension( - 'affine:page', - AFFINE_EDGELESS_COPILOT_WIDGET, - literal`${unsafeStatic(AFFINE_EDGELESS_COPILOT_WIDGET)}` -); - -export function createAIPageRootBlockSpec( - framework: FrameworkProvider -): ExtensionType[] { - return [...PageRootBlockSpec, aiPanelWidget, getAIPageRootWatcher(framework)]; -} - -function getAIEdgelessRootWatcher(framework: FrameworkProvider) { - class AIEdgelessRootWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:page'; - - override mounted() { - super.mounted(); - this.blockService.specSlots.widgetConnected.on(view => { - if (view.component instanceof AffineAIPanelWidget) { - view.component.style.width = '430px'; - view.component.config = buildAIPanelConfig(view.component, framework); - setupSpaceAIEntry(view.component); - } - - if (view.component instanceof EdgelessCopilotWidget) { - setupEdgelessCopilot(view.component); - } - - if (view.component instanceof EdgelessElementToolbarWidget) { - setupEdgelessElementToolbarAIEntry(view.component); - } - - if (view.component instanceof AffineFormatBarWidget) { - setupFormatBarAIEntry(view.component); - } - - if (view.component instanceof AffineSlashMenuWidget) { - setupSlashMenuAIEntry(view.component); - } - }); - } - } - return AIEdgelessRootWatcher; -} - -export function createAIEdgelessRootBlockSpec( - framework: FrameworkProvider -): ExtensionType[] { - return [ - ...EdgelessRootBlockSpec, - CopilotTool, - aiPanelWidget, - edgelessCopilotWidget, - getAIEdgelessRootWatcher(framework), - ]; -} - -class AIParagraphBlockWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:paragraph'; - - override mounted() { - super.mounted(); - const service = this.blockService; - assertInstanceOf(service, ParagraphBlockService); - service.placeholderGenerator = model => { - if (model.type === 'text') { - return "Type '/' for commands, 'space' for AI"; - } - - const placeholders = { - h1: 'Heading 1', - h2: 'Heading 2', - h3: 'Heading 3', - h4: 'Heading 4', - h5: 'Heading 5', - h6: 'Heading 6', - quote: '', - }; - return placeholders[model.type]; - }; - } -} - -export const AIParagraphBlockSpec: ExtensionType[] = [ - ...ParagraphBlockSpec, - AIParagraphBlockWatcher, -]; - -class AICodeBlockWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:code'; - - override mounted() { - super.mounted(); - const service = this.blockService; - service.specSlots.widgetConnected.on(view => { - if (view.component instanceof AffineCodeToolbarWidget) { - setupCodeToolbarAIEntry(view.component); - } - }); - } -} - -export const AICodeBlockSpec: ExtensionType[] = [ - ...CodeBlockSpec, - AICodeBlockWatcher, -]; - -class AIImageBlockWatcher extends BlockServiceWatcher { - static override readonly flavour = 'affine:image'; - - override mounted() { - super.mounted(); - this.blockService.specSlots.widgetConnected.on(view => { - if (view.component instanceof AffineImageToolbarWidget) { - setupImageToolbarAIEntry(view.component); - } - }); - } -} - -export const AIImageBlockSpec: ExtensionType[] = [ - ...ImageBlockSpec, - AIImageBlockWatcher, -]; diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts new file mode 100644 index 0000000000..188c9dd101 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-code.ts @@ -0,0 +1,27 @@ +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { + AffineCodeToolbarWidget, + CodeBlockSpec, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +import { setupCodeToolbarAIEntry } from '../entries/code-toolbar/setup-code-toolbar'; + +class AICodeBlockWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:code'; + + override mounted() { + super.mounted(); + const service = this.blockService; + service.specSlots.widgetConnected.on(view => { + if (view.component instanceof AffineCodeToolbarWidget) { + setupCodeToolbarAIEntry(view.component); + } + }); + } +} + +export const AICodeBlockSpec: ExtensionType[] = [ + ...CodeBlockSpec, + AICodeBlockWatcher, +]; diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts new file mode 100644 index 0000000000..9dafec75d4 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-edgeless-root.ts @@ -0,0 +1,73 @@ +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { + AffineFormatBarWidget, + AffineSlashMenuWidget, + EdgelessElementToolbarWidget, + EdgelessRootBlockSpec, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; +import type { FrameworkProvider } from '@toeverything/infra'; + +import { buildAIPanelConfig } from '../ai-panel'; +import { + setupEdgelessCopilot, + setupEdgelessElementToolbarAIEntry, +} from '../entries/edgeless/index'; +import { setupFormatBarAIEntry } from '../entries/format-bar/setup-format-bar'; +import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu'; +import { setupSpaceAIEntry } from '../entries/space/setup-space'; +import { CopilotTool } from '../tool/copilot-tool'; +import { + AffineAIPanelWidget, + aiPanelWidget, +} from '../widgets/ai-panel/ai-panel'; +import { + EdgelessCopilotWidget, + edgelessCopilotWidget, +} from '../widgets/edgeless-copilot'; + +export function createAIEdgelessRootBlockSpec( + framework: FrameworkProvider +): ExtensionType[] { + return [ + ...EdgelessRootBlockSpec, + CopilotTool, + aiPanelWidget, + edgelessCopilotWidget, + getAIEdgelessRootWatcher(framework), + ]; +} + +function getAIEdgelessRootWatcher(framework: FrameworkProvider) { + class AIEdgelessRootWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:page'; + + override mounted() { + super.mounted(); + this.blockService.specSlots.widgetConnected.on(view => { + if (view.component instanceof AffineAIPanelWidget) { + view.component.style.width = '430px'; + view.component.config = buildAIPanelConfig(view.component, framework); + setupSpaceAIEntry(view.component); + } + + if (view.component instanceof EdgelessCopilotWidget) { + setupEdgelessCopilot(view.component); + } + + if (view.component instanceof EdgelessElementToolbarWidget) { + setupEdgelessElementToolbarAIEntry(view.component); + } + + if (view.component instanceof AffineFormatBarWidget) { + setupFormatBarAIEntry(view.component); + } + + if (view.component instanceof AffineSlashMenuWidget) { + setupSlashMenuAIEntry(view.component); + } + }); + } + } + return AIEdgelessRootWatcher; +} diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-image.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-image.ts new file mode 100644 index 0000000000..9d887e4b16 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-image.ts @@ -0,0 +1,26 @@ +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { + AffineImageToolbarWidget, + ImageBlockSpec, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +import { setupImageToolbarAIEntry } from '../entries/image-toolbar/setup-image-toolbar'; + +class AIImageBlockWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:image'; + + override mounted() { + super.mounted(); + this.blockService.specSlots.widgetConnected.on(view => { + if (view.component instanceof AffineImageToolbarWidget) { + setupImageToolbarAIEntry(view.component); + } + }); + } +} + +export const AIImageBlockSpec: ExtensionType[] = [ + ...ImageBlockSpec, + AIImageBlockWatcher, +]; diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts new file mode 100644 index 0000000000..da8f40a24e --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-page-root.ts @@ -0,0 +1,49 @@ +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { + AffineFormatBarWidget, + AffineSlashMenuWidget, + PageRootBlockSpec, +} from '@blocksuite/affine/blocks'; +import type { ExtensionType } from '@blocksuite/affine/store'; +import type { FrameworkProvider } from '@toeverything/infra'; + +import { buildAIPanelConfig } from '../ai-panel'; +import { setupFormatBarAIEntry } from '../entries/format-bar/setup-format-bar'; +import { setupSlashMenuAIEntry } from '../entries/slash-menu/setup-slash-menu'; +import { setupSpaceAIEntry } from '../entries/space/setup-space'; +import { + AffineAIPanelWidget, + aiPanelWidget, +} from '../widgets/ai-panel/ai-panel'; + +function getAIPageRootWatcher(framework: FrameworkProvider) { + class AIPageRootWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:page'; + + override mounted() { + super.mounted(); + this.blockService.specSlots.widgetConnected.on(view => { + if (view.component instanceof AffineAIPanelWidget) { + view.component.style.width = '630px'; + view.component.config = buildAIPanelConfig(view.component, framework); + setupSpaceAIEntry(view.component); + } + + if (view.component instanceof AffineFormatBarWidget) { + setupFormatBarAIEntry(view.component); + } + + if (view.component instanceof AffineSlashMenuWidget) { + setupSlashMenuAIEntry(view.component); + } + }); + } + } + return AIPageRootWatcher; +} + +export function createAIPageRootBlockSpec( + framework: FrameworkProvider +): ExtensionType[] { + return [...PageRootBlockSpec, aiPanelWidget, getAIPageRootWatcher(framework)]; +} diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/ai-paragraph.ts b/packages/frontend/core/src/blocksuite/ai/extensions/ai-paragraph.ts new file mode 100644 index 0000000000..58ed949900 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/ai-paragraph.ts @@ -0,0 +1,38 @@ +import { BlockServiceWatcher } from '@blocksuite/affine/block-std'; +import { + ParagraphBlockService, + ParagraphBlockSpec, +} from '@blocksuite/affine/blocks'; +import { assertInstanceOf } from '@blocksuite/affine/global/utils'; +import type { ExtensionType } from '@blocksuite/affine/store'; + +class AIParagraphBlockWatcher extends BlockServiceWatcher { + static override readonly flavour = 'affine:paragraph'; + + override mounted() { + super.mounted(); + const service = this.blockService; + assertInstanceOf(service, ParagraphBlockService); + service.placeholderGenerator = model => { + if (model.type === 'text') { + return "Type '/' for commands, 'space' for AI"; + } + + const placeholders = { + h1: 'Heading 1', + h2: 'Heading 2', + h3: 'Heading 3', + h4: 'Heading 4', + h5: 'Heading 5', + h6: 'Heading 6', + quote: '', + }; + return placeholders[model.type]; + }; + } +} + +export const AIParagraphBlockSpec: ExtensionType[] = [ + ...ParagraphBlockSpec, + AIParagraphBlockWatcher, +]; diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts b/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts new file mode 100644 index 0000000000..5db82b9674 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/enable-ai.ts @@ -0,0 +1,37 @@ +import { + CodeBlockSpec, + EdgelessRootBlockSpec, + ImageBlockSpec, + PageRootBlockSpec, + ParagraphBlockSpec, + type SpecBuilder, +} from '@blocksuite/affine/blocks'; +import type { FrameworkProvider } from '@toeverything/infra'; + +import { AIChatBlockSpec } from '../blocks'; +import { AICodeBlockSpec } from './ai-code'; +import { createAIEdgelessRootBlockSpec } from './ai-edgeless-root'; +import { AIImageBlockSpec } from './ai-image'; +import { createAIPageRootBlockSpec } from './ai-page-root'; +import { AIParagraphBlockSpec } from './ai-paragraph'; + +export function enableAIExtension( + specBuilder: SpecBuilder, + framework: FrameworkProvider +) { + specBuilder.replace(CodeBlockSpec, AICodeBlockSpec); + specBuilder.replace(ImageBlockSpec, AIImageBlockSpec); + specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec); + + if (specBuilder.hasAll(EdgelessRootBlockSpec)) { + const aiEdgeless = createAIEdgelessRootBlockSpec(framework); + specBuilder.replace(EdgelessRootBlockSpec, aiEdgeless); + } + + if (specBuilder.hasAll(PageRootBlockSpec)) { + const aiPage = createAIPageRootBlockSpec(framework); + specBuilder.replace(PageRootBlockSpec, aiPage); + } + + specBuilder.extend(AIChatBlockSpec); +} diff --git a/packages/frontend/core/src/blocksuite/ai/extensions/index.ts b/packages/frontend/core/src/blocksuite/ai/extensions/index.ts new file mode 100644 index 0000000000..566868dc50 --- /dev/null +++ b/packages/frontend/core/src/blocksuite/ai/extensions/index.ts @@ -0,0 +1 @@ +export * from './enable-ai'; diff --git a/packages/frontend/core/src/blocksuite/ai/index.ts b/packages/frontend/core/src/blocksuite/ai/index.ts index 9a7f27cd2f..44cedbe710 100644 --- a/packages/frontend/core/src/blocksuite/ai/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/index.ts @@ -1,10 +1,10 @@ export * from './_common/config'; -export * from './actions/index'; -export * from './ai-spec'; -export { ChatPanel } from './chat-panel/index'; +export * from './actions'; +export { ChatPanel } from './chat-panel'; +export * from './entries'; export * from './entries/edgeless/actions-config'; -export * from './entries/index'; -export * from './messages/index'; +export * from './extensions'; +export * from './messages'; export { AIChatBlockPeekViewTemplate } from './peek-view/chat-block-peek-view'; export * from './provider'; export * from './utils/edgeless'; diff --git a/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/ai-panel.ts b/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/ai-panel.ts index 9438086043..858d4869de 100644 --- a/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/ai-panel.ts +++ b/packages/frontend/core/src/blocksuite/ai/widgets/ai-panel/ai-panel.ts @@ -1,4 +1,7 @@ -import { WidgetComponent } from '@blocksuite/affine/block-std'; +import { + WidgetComponent, + WidgetViewExtension, +} from '@blocksuite/affine/block-std'; import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx'; import { AFFINE_FORMAT_BAR_WIDGET, @@ -25,6 +28,7 @@ import { import { css, html, nothing, type PropertyValues } from 'lit'; import { property, query } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; +import { literal, unsafeStatic } from 'lit/static-html.js'; import type { AIError } from '../../components/ai-item/types.js'; import type { AIPanelGenerating } from './components/index.js'; @@ -543,3 +547,9 @@ export class AffineAIPanelWidget extends WidgetComponent { @property() accessor state: AffineAIPanelState = 'hidden'; } + +export const aiPanelWidget = WidgetViewExtension( + 'affine:page', + AFFINE_AI_PANEL_WIDGET, + literal`${unsafeStatic(AFFINE_AI_PANEL_WIDGET)}` +); diff --git a/packages/frontend/core/src/blocksuite/ai/widgets/edgeless-copilot/index.ts b/packages/frontend/core/src/blocksuite/ai/widgets/edgeless-copilot/index.ts index 2c74a6ccde..36050f54a4 100644 --- a/packages/frontend/core/src/blocksuite/ai/widgets/edgeless-copilot/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/widgets/edgeless-copilot/index.ts @@ -1,4 +1,7 @@ -import { WidgetComponent } from '@blocksuite/affine/block-std'; +import { + WidgetComponent, + WidgetViewExtension, +} from '@blocksuite/affine/block-std'; import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx'; import type { RootBlockModel } from '@blocksuite/affine/blocks'; import { @@ -22,6 +25,7 @@ import { effect } from '@preact/signals-core'; import { css, html, nothing } from 'lit'; import { query, state } from 'lit/decorators.js'; import { styleMap } from 'lit/directives/style-map.js'; +import { literal, unsafeStatic } from 'lit/static-html.js'; import type { AIItemGroupConfig } from '../../components/ai-item/types.js'; import { @@ -290,6 +294,12 @@ export class EdgelessCopilotWidget extends WidgetComponent { accessor selectionElem!: HTMLDivElement; } +export const edgelessCopilotWidget = WidgetViewExtension( + 'affine:page', + AFFINE_EDGELESS_COPILOT_WIDGET, + literal`${unsafeStatic(AFFINE_EDGELESS_COPILOT_WIDGET)}` +); + declare global { interface HTMLElementTagNameMap { [AFFINE_EDGELESS_COPILOT_WIDGET]: EdgelessCopilotWidget; diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/common.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/common.ts deleted file mode 100644 index 39042e5e47..0000000000 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/common.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { - AICodeBlockSpec, - AIImageBlockSpec, - AIParagraphBlockSpec, -} from '@affine/core/blocksuite/ai'; -import { AIChatBlockSpec } from '@affine/core/blocksuite/ai/blocks'; -import { - AdapterFactoryExtensions, - AttachmentBlockSpec, - BookmarkBlockSpec, - CodeBlockSpec, - DatabaseBlockSpec, - DataViewBlockSpec, - DefaultOpenDocExtension, - DividerBlockSpec, - EditPropsStore, - EmbedExtensions, - FontLoaderService, - ImageBlockSpec, - LatexBlockSpec, - ListBlockSpec, - ParagraphBlockSpec, - RefNodeSlotsExtension, - RichTextExtensions, - TableBlockSpec, -} from '@blocksuite/affine/blocks'; -import type { ExtensionType } from '@blocksuite/affine/store'; - -const CommonBlockSpecs: ExtensionType[] = [ - RefNodeSlotsExtension, - EditPropsStore, - RichTextExtensions, - LatexBlockSpec, - ListBlockSpec, - DatabaseBlockSpec, - TableBlockSpec, - DataViewBlockSpec, - DividerBlockSpec, - EmbedExtensions, - BookmarkBlockSpec, - AttachmentBlockSpec, - AdapterFactoryExtensions, - FontLoaderService, - DefaultOpenDocExtension, -].flat(); - -export const DefaultBlockSpecs: ExtensionType[] = [ - CodeBlockSpec, - ImageBlockSpec, - ParagraphBlockSpec, - ...CommonBlockSpecs, -].flat(); - -export const AIBlockSpecs: ExtensionType[] = [ - AICodeBlockSpec, - AIImageBlockSpec, - AIParagraphBlockSpec, - AIChatBlockSpec, - ...CommonBlockSpecs, -].flat(); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts index 234a2ee267..97f31275a1 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/custom/root-block.ts @@ -1,9 +1,3 @@ -import { - AICodeBlockSpec, - AIImageBlockSpec, - AIParagraphBlockSpec, -} from '@affine/core/blocksuite/ai'; -import { AIChatBlockSpec } from '@affine/core/blocksuite/ai/blocks'; import { DocService, DocsService } from '@affine/core/modules/doc'; import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta'; import { EditorSettingService } from '@affine/core/modules/editor-setting'; @@ -19,14 +13,11 @@ import type { ThemeExtension, } from '@blocksuite/affine/blocks'; import { - CodeBlockSpec, ColorScheme, createSignalFromObservable, DatabaseConfigExtension, DocDisplayMetaProvider, EditorSettingExtension, - ImageBlockSpec, - ParagraphBlockSpec, referenceToNode, RootBlockConfigExtension, SpecProvider, @@ -267,10 +258,3 @@ export function enableAffineExtension( ].flat() ); } - -export function enableAIExtension(specBuilder: SpecBuilder): void { - specBuilder.replace(CodeBlockSpec, AICodeBlockSpec); - specBuilder.replace(ImageBlockSpec, AIImageBlockSpec); - specBuilder.replace(ParagraphBlockSpec, AIParagraphBlockSpec); - specBuilder.extend(AIChatBlockSpec); -} diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts index c6819f1750..50f2ade690 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/edgeless.ts @@ -1,16 +1,12 @@ -import { createAIEdgelessRootBlockSpec } from '@affine/core/blocksuite/ai'; +import { enableAIExtension } from '@affine/core/blocksuite/ai'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; import { builtInTemplates as builtInEdgelessTemplates } from '@affine/templates/edgeless'; import { builtInTemplates as builtInStickersTemplates } from '@affine/templates/stickers'; import type { SpecBuilder, TemplateManager } from '@blocksuite/affine/blocks'; -import { - EdgelessRootBlockSpec, - EdgelessTemplatePanel, - SpecProvider, -} from '@blocksuite/affine/blocks'; +import { EdgelessTemplatePanel, SpecProvider } from '@blocksuite/affine/blocks'; import { type FrameworkProvider } from '@toeverything/infra'; -import { enableAffineExtension, enableAIExtension } from './custom/root-block'; +import { enableAffineExtension } from './custom/root-block'; export function createEdgelessModeSpecs( framework: FrameworkProvider @@ -20,11 +16,7 @@ export function createEdgelessModeSpecs( const edgelessSpec = SpecProvider._.getSpec('edgeless'); enableAffineExtension(framework, edgelessSpec); if (enableAI) { - enableAIExtension(edgelessSpec); - edgelessSpec.replace( - EdgelessRootBlockSpec, - createAIEdgelessRootBlockSpec(framework) - ); + enableAIExtension(edgelessSpec, framework); } return edgelessSpec; diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts index 24287809a7..ea5ee6fecf 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/specs/page.ts @@ -1,13 +1,9 @@ -import { createAIPageRootBlockSpec } from '@affine/core/blocksuite/ai'; +import { enableAIExtension } from '@affine/core/blocksuite/ai'; import { FeatureFlagService } from '@affine/core/modules/feature-flag'; -import { - PageRootBlockSpec, - type SpecBuilder, - SpecProvider, -} from '@blocksuite/affine/blocks'; +import { type SpecBuilder, SpecProvider } from '@blocksuite/affine/blocks'; import { type FrameworkProvider } from '@toeverything/infra'; -import { enableAffineExtension, enableAIExtension } from './custom/root-block'; +import { enableAffineExtension } from './custom/root-block'; export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder { const featureFlagService = framework.get(FeatureFlagService); @@ -16,8 +12,7 @@ export function createPageModeSpecs(framework: FrameworkProvider): SpecBuilder { const pageSpec = provider.getSpec('page'); enableAffineExtension(framework, pageSpec); if (enableAI) { - enableAIExtension(pageSpec); - pageSpec.replace(PageRootBlockSpec, createAIPageRootBlockSpec(framework)); + enableAIExtension(pageSpec, framework); } return pageSpec; }