From 8b4c8b995261c7f23092e6843f0e3e4956d0c0cb Mon Sep 17 00:00:00 2001 From: doouding Date: Wed, 24 Apr 2024 07:10:01 +0000 Subject: [PATCH 1/2] feat(edgeless): expand mindmap action (#6852) - Fix group & mindmap issue - Add expand action - Naming --- packages/blocks/src/index.ts | 2 +- .../root-block/widgets/ai-panel/ai-panel.ts | 55 +++-- .../element-renderer/shape/utils.ts | 2 +- .../src/surface-block/element-model/base.ts | 6 +- .../surface-block/element-model/mindmap.ts | 59 ++++-- packages/blocks/src/surface-block/index.ts | 2 +- .../src/surface-block/middlewares/group.ts | 15 +- .../src/surface-block/middlewares/mindmap.ts | 36 ++-- .../mini-mindmap/surface-block.ts | 4 +- .../blocks/src/surface-block/surface-model.ts | 30 ++- .../src/__tests__/edgeless/group.spec.ts | 195 +++++++++++++++++- .../src/ai/actions/edgeless-handler.ts | 42 +++- .../src/ai/actions/edgeless-response.ts | 101 +++++++-- packages/presets/src/ai/actions/types.ts | 6 +- .../src/ai/entries/edgeless/actions-config.ts | 64 ++++-- packages/presets/src/ai/utils/edgeless.ts | 34 +++ 16 files changed, 530 insertions(+), 123 deletions(-) create mode 100644 packages/presets/src/ai/utils/edgeless.ts diff --git a/packages/blocks/src/index.ts b/packages/blocks/src/index.ts index ae87b05c03736..4a2a11d590a44 100644 --- a/packages/blocks/src/index.ts +++ b/packages/blocks/src/index.ts @@ -114,6 +114,7 @@ export { ConnectorElementModel, ConnectorMode, ElementModel, + fitContent, generateKeyBetween, GroupElementModel, MindmapElementModel, @@ -127,7 +128,6 @@ export { StrokeStyle, SurfaceBlockModel, TextElementModel, - updateMindmapNodeRect, } from './surface-block/index.js'; export { MiniMindmapPreview } from './surface-block/mini-mindmap/mindmap-preview.js'; export { SurfaceBlockComponent } from './surface-block/surface-block.js'; diff --git a/packages/blocks/src/root-block/widgets/ai-panel/ai-panel.ts b/packages/blocks/src/root-block/widgets/ai-panel/ai-panel.ts index 8d3543545d062..829ac0430bf15 100644 --- a/packages/blocks/src/root-block/widgets/ai-panel/ai-panel.ts +++ b/packages/blocks/src/root-block/widgets/ai-panel/ai-panel.ts @@ -13,7 +13,7 @@ import { customElement, property, query } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; import type { AIError } from '../../../_common/components/index.js'; -import { on, stopPropagation } from '../../../_common/utils/event.js'; +import { stopPropagation } from '../../../_common/utils/event.js'; import type { AIPanelDiscardModal } from './components/discard-modal.js'; import { toggleDiscardModal } from './components/discard-modal.js'; import type { AffineAIPanelState, AffineAIPanelWidgetConfig } from './type.js'; @@ -118,15 +118,7 @@ export class AffineAIPanelWidget extends WidgetElement { this.state = 'input'; } - this._stopAutoUpdate?.(); - this._stopAutoUpdate = autoUpdate(reference, this, () => { - computePosition(reference, this, this.config?.positionConfig) - .then(({ x, y }) => { - this.style.left = `${x}px`; - this.style.top = `${y}px`; - }) - .catch(console.error); - }); + this._autoUpdatePosition(reference); }; hide = () => { @@ -218,13 +210,42 @@ export class AffineAIPanelWidget extends WidgetElement { this.generate(); }; + private _autoUpdatePosition(reference: ReferenceElement) { + this._stopAutoUpdate?.(); + this._stopAutoUpdate = autoUpdate(reference, this, () => { + computePosition(reference, this, this.config?.positionConfig) + .then(({ x, y }) => { + this.style.left = `${x}px`; + this.style.top = `${y}px`; + }) + .catch(console.error); + }); + } + override connectedCallback() { super.connectedCallback(); this.tabIndex = -1; - this.disposables.add(on(this, 'wheel', stopPropagation)); - this.disposables.add(on(this, 'pointerdown', stopPropagation)); - this.disposables.addFromEvent(document, 'mousedown', this._onDocumentClick); + this.disposables.addFromEvent( + document, + 'pointerdown', + this._onDocumentClick + ); + this.disposables.add( + this.blockElement.host.event.add('pointerDown', evtState => + this._onDocumentClick( + evtState.get('pointerState').event as PointerEvent + ) + ) + ); + this.disposables.add( + this.blockElement.host.event.add('click', () => { + return this.state !== 'hidden' ? true : false; + }) + ); + this.disposables.addFromEvent(this, 'wheel', stopPropagation); + this.disposables.addFromEvent(this, 'pointerdown', stopPropagation); + this.disposables.addFromEvent(this, 'pointerup', stopPropagation); } override disconnectedCallback() { @@ -233,17 +254,17 @@ export class AffineAIPanelWidget extends WidgetElement { } private _onDocumentClick = (e: MouseEvent) => { - if (this.state !== 'hidden') { - e.preventDefault(); - } - if ( + this.state !== 'hidden' && e.target !== this._discardModal && e.target !== this && !this.contains(e.target as Node) ) { this._clickOutside(); + return true; } + + return false; }; protected override willUpdate(changed: PropertyValues): void { diff --git a/packages/blocks/src/surface-block/canvas-renderer/element-renderer/shape/utils.ts b/packages/blocks/src/surface-block/canvas-renderer/element-renderer/shape/utils.ts index 729d5ab3836fc..b1cff87279039 100644 --- a/packages/blocks/src/surface-block/canvas-renderer/element-renderer/shape/utils.ts +++ b/packages/blocks/src/surface-block/canvas-renderer/element-renderer/shape/utils.ts @@ -188,7 +188,7 @@ export function normalizeShapeBound( return bound; } -export function updateMindmapNodeRect(shape: ShapeElementModel) { +export function fitContent(shape: ShapeElementModel) { const font = getFontString(shape); if (!shape.text) { diff --git a/packages/blocks/src/surface-block/element-model/base.ts b/packages/blocks/src/surface-block/element-model/base.ts index 48618053f717d..f9b4846966c17 100644 --- a/packages/blocks/src/surface-block/element-model/base.ts +++ b/packages/blocks/src/surface-block/element-model/base.ts @@ -192,6 +192,10 @@ export abstract class ElementModel return Bound.deserialize(this.xywh); } + get isConnected() { + return this.surface.hasElementById(this.id); + } + stash(prop: keyof Props | string) { if (this._stashed.has(prop)) { return; @@ -384,7 +388,7 @@ export abstract class GroupLikeModel< xywh: SerializedXYWH = '[0,0,0,0]'; /** - * Check if the element has the descendant + * Check if the group has the given descendant. */ hasDescendant(element: string | EdgelessModel) { const groups = diff --git a/packages/blocks/src/surface-block/element-model/mindmap.ts b/packages/blocks/src/surface-block/element-model/mindmap.ts index 8772efd950f64..585720ff2512f 100644 --- a/packages/blocks/src/surface-block/element-model/mindmap.ts +++ b/packages/blocks/src/surface-block/element-model/mindmap.ts @@ -34,6 +34,7 @@ import { const baseNodeSchema = z.object({ text: z.string(), + xywh: z.optional(z.string()), }); type Node = z.infer & { @@ -83,7 +84,7 @@ export class MindmapElementModel extends GroupLikeModel { const id = surface.addElement({ type: 'shape', text: node.text, - xywh: `[0, 0, 100, 30]`, + xywh: node.xywh ? node.xywh : `[0, 0, 100, 30]`, }); map.set(id, { @@ -261,6 +262,10 @@ export class MindmapElementModel extends GroupLikeModel { return node?.parent ? this.surface.getElementById(node.parent) : null; } + getNode(id: string) { + return this._nodeMap.get(id) ?? null; + } + /** * Path is an array of indexes that represent the path from the root node to the target node. * The first element of the array is always 0, which represents the root node. @@ -273,7 +278,7 @@ export class MindmapElementModel extends GroupLikeModel { * // [0, 1, 2] * ``` */ - getPath(element: string | MindmapNode) { + private _getPath(element: string | MindmapNode) { let node = this._nodeMap.get( typeof element === 'string' ? element : element.id ); @@ -330,8 +335,8 @@ export class MindmapElementModel extends GroupLikeModel { sibling = sibling ?? last(parentNode.children)?.id; const siblingNode = sibling ? this._nodeMap.get(sibling) : undefined; const path = siblingNode - ? this.getPath(siblingNode) - : this.getPath(parentNode).concat([0]); + ? this._getPath(siblingNode) + : this._getPath(parentNode).concat([0]); const style = this.styleGetter.getNodeStyle( siblingNode ?? parentNode, path @@ -401,27 +406,47 @@ export class MindmapElementModel extends GroupLikeModel { return id!; } - removeDescendant(id: string, transaction: boolean = true) { + addTree(parent: string | null, tree: NodeType, sibling?: string) { + const traverse = ( + node: NodeType, + parent: string | null, + sibling?: string + ) => { + const nodeId = this.addNode(parent, 'shape', sibling, 'after', { + text: node.text, + }); + + node.children?.forEach(child => { + traverse(child, nodeId); + }); + + return nodeId; + }; + + return traverse(tree, parent, sibling); + } + + removeDescendant(id: string) { if (!this._nodeMap.has(id)) { return; } - const node = this._nodeMap.get(id)!; - const remove = () => { - node.children.forEach(child => { - this.removeDescendant(child.id, false); + const surface = this.surface; + const removedDescendants: string[] = []; + const remove = (element: MindmapNode) => { + element.children?.forEach(child => { + remove(child); }); - this.children.delete(id); + this.children.delete(element.id); + removedDescendants.push(element.id); }; - if (transaction) { - this.surface.doc.transact(() => { - remove(); - }); - } else { - remove(); - } + surface.doc.transact(() => { + remove(this._nodeMap.get(id)!); + this.setChildIds(Array.from(this.children.keys()), true); + removedDescendants.forEach(id => surface.removeElement(id)); + }); } layout() { diff --git a/packages/blocks/src/surface-block/index.ts b/packages/blocks/src/surface-block/index.ts index 83deadac98ad0..f9da24b37920d 100644 --- a/packages/blocks/src/surface-block/index.ts +++ b/packages/blocks/src/surface-block/index.ts @@ -2,7 +2,7 @@ import type { SurfaceBlockModel } from './surface-model.js'; import type { SurfaceService } from './surface-service.js'; export { normalizeShapeBound } from './canvas-renderer/element-renderer/index.js'; -export { updateMindmapNodeRect } from './canvas-renderer/element-renderer/shape/utils.js'; +export { fitContent } from './canvas-renderer/element-renderer/shape/utils.js'; export { Overlay, Renderer } from './canvas-renderer/renderer.js'; export { type IBound, diff --git a/packages/blocks/src/surface-block/middlewares/group.ts b/packages/blocks/src/surface-block/middlewares/group.ts index c902ebefd835e..f9f1d9b31e300 100644 --- a/packages/blocks/src/surface-block/middlewares/group.ts +++ b/packages/blocks/src/surface-block/middlewares/group.ts @@ -39,13 +39,26 @@ export const groupSizeMiddleware: SurfaceMiddleware = ( }; const disposables = [ + surface.doc.slots.blockUpdated.on(payload => { + if (payload.type === 'update') { + const group = surface.getGroup(payload.id); + + if (group instanceof GroupLikeModel && payload.props.key === 'xywh') { + addGroupSizeUpdateList(group); + } + } + }), surface.elementUpdated.on(({ id, props }) => { // update the group's xywh when children's xywh updated const group = surface.getGroup(id); - if (group instanceof GroupLikeModel && props['xywh']) { addGroupSizeUpdateList(group); } + + const element = surface.getElementById(id); + if (element instanceof GroupLikeModel && props['childIds']) { + addGroupSizeUpdateList(element); + } }), surface.elementAdded.on(({ id }) => { // update the group's xywh when added diff --git a/packages/blocks/src/surface-block/middlewares/mindmap.ts b/packages/blocks/src/surface-block/middlewares/mindmap.ts index aa2b12c2aab6f..0932258fb4bce 100644 --- a/packages/blocks/src/surface-block/middlewares/mindmap.ts +++ b/packages/blocks/src/surface-block/middlewares/mindmap.ts @@ -43,8 +43,10 @@ export const mindmapMiddleware: SurfaceMiddleware = ( connUpdPending = true; queueMicrotask(() => { connUpdList.forEach(mindmap => { - mindmap['buildTree'](); - mindmap.calcConnection(); + if (mindmap.isConnected) { + mindmap['buildTree'](); + mindmap.calcConnection(); + } }); connUpdList.clear(); connUpdPending = false; @@ -52,23 +54,21 @@ export const mindmapMiddleware: SurfaceMiddleware = ( }; const disposables = [ - surface.elementAdded - .filter(payload => payload.local) - .on(({ id }) => { - /** - * When loading an existing doc, the elements' loading order is not guaranteed - * So we need to update the mindmap when a new element is added - */ - const group = surface.getGroup(id); - if (group instanceof MindmapElementModel) { - updateConnection(group); - } + surface.elementAdded.on(({ id }) => { + /** + * When loading an existing doc, the elements' loading order is not guaranteed + * So we need to update the mindmap when a new element is added + */ + const group = surface.getGroup(id); + if (group instanceof MindmapElementModel) { + updateConnection(group); + } - const element = surface.getElementById(id); - if (element instanceof MindmapElementModel) { - updateConnection(element); - } - }), + const element = surface.getElementById(id); + if (element instanceof MindmapElementModel) { + updateConnection(element); + } + }), surface.elementUpdated.on(({ id, props }) => { if (props['childIds'] || props['style']) { const element = surface.getElementById(id); diff --git a/packages/blocks/src/surface-block/mini-mindmap/surface-block.ts b/packages/blocks/src/surface-block/mini-mindmap/surface-block.ts index c7ff1038fe015..71637a7ab3663 100644 --- a/packages/blocks/src/surface-block/mini-mindmap/surface-block.ts +++ b/packages/blocks/src/surface-block/mini-mindmap/surface-block.ts @@ -3,7 +3,7 @@ import { html } from 'lit'; import { customElement, query } from 'lit/decorators.js'; import { ThemeObserver } from '../../_common/theme/theme-observer.js'; -import { updateMindmapNodeRect } from '../canvas-renderer/element-renderer/shape/utils.js'; +import { fitContent } from '../canvas-renderer/element-renderer/shape/utils.js'; import { Renderer } from '../canvas-renderer/renderer.js'; import type { ShapeElementModel } from '../element-model/shape.js'; import { LayerManager } from '../managers/layer-manager.js'; @@ -30,7 +30,7 @@ export class MindmapSurfaceBlock extends BlockElement { this.model.doc.transact(() => { this.model.elementModels.forEach(element => { if (element.type === 'shape') { - updateMindmapNodeRect(element as ShapeElementModel); + fitContent(element as ShapeElementModel); } }); }); diff --git a/packages/blocks/src/surface-block/surface-model.ts b/packages/blocks/src/surface-block/surface-model.ts index 487f0f213d5d3..3c6bba82c1a97 100644 --- a/packages/blocks/src/surface-block/surface-model.ts +++ b/packages/blocks/src/surface-block/surface-model.ts @@ -1,4 +1,4 @@ -import { Slot } from '@blocksuite/global/utils'; +import { DisposableGroup, Slot } from '@blocksuite/global/utils'; import type { MigrationRunner, Y } from '@blocksuite/store'; import { BlockModel, @@ -180,7 +180,7 @@ export class SurfaceBlockModel extends BlockModel { string, { mount: () => void; unmount: () => void; model: ElementModel } > = new Map(); - private _disposables: Array<() => void> = []; + private _disposables: DisposableGroup = new DisposableGroup(); private _groupToElements: Map = new Map(); private _elementToGroup: Map = new Map(); private _connectorToElements: Map = new Map(); @@ -223,12 +223,12 @@ export class SurfaceBlockModel extends BlockModel { } private _applyMiddlewares() { - this._disposables.push( + [ connectorMiddleware(this, this.hooks), groupRelationMiddleware(this, this.hooks), groupSizeMiddleware(this, this.hooks), - mindmapMiddleware(this, this.hooks) - ); + mindmapMiddleware(this, this.hooks), + ].forEach(disposable => this._disposables.add(disposable)); } private _initElementModels() { @@ -301,7 +301,7 @@ export class SurfaceBlockModel extends BlockModel { }); elementsYMap.observe(onElementsMapChange); - this._disposables.push(() => { + this._disposables.add(() => { elementsYMap.unobserve(onElementsMapChange); }); } @@ -380,6 +380,20 @@ export class SurfaceBlockModel extends BlockModel { children.forEach(childId => removeFromGroup(childId, id)); } }); + + this._disposables.add( + this.doc.slots.blockUpdated.on(({ type, id }) => { + switch (type) { + case 'delete': { + const group = this.getGroup(id); + + if (group) { + group.removeDescendant(id); + } + } + } + }) + ); } private _watchConnectorRelationChange() { @@ -474,7 +488,7 @@ export class SurfaceBlockModel extends BlockModel { override dispose(): void { super.dispose(); - this._disposables.forEach(dispose => dispose()); + this._disposables.dispose(); this.elementAdded.dispose(); this.elementRemoved.dispose(); @@ -565,7 +579,7 @@ export class SurfaceBlockModel extends BlockModel { throw new Error('Cannot remove element in readonly mode'); } - if (!this.getElementById(id)) { + if (!this.hasElementById(id)) { return; } diff --git a/packages/presets/src/__tests__/edgeless/group.spec.ts b/packages/presets/src/__tests__/edgeless/group.spec.ts index 851b26a908e2e..111872a04af0d 100644 --- a/packages/presets/src/__tests__/edgeless/group.spec.ts +++ b/packages/presets/src/__tests__/edgeless/group.spec.ts @@ -1,7 +1,15 @@ -import type { EdgelessRootBlockComponent } from '@blocksuite/blocks'; +import type { + GroupElementModel, + MindmapElementModel, +} from '@blocksuite/blocks'; +import { + type EdgelessRootBlockComponent, + NoteDisplayMode, +} from '@blocksuite/blocks'; import { DocCollection } from '@blocksuite/store'; import { beforeEach, describe, expect, test } from 'vitest'; +import { wait } from '../utils/common.js'; import { addNote, getDocRootBlock } from '../utils/edgeless.js'; import { setupEditor } from '../utils/setup.js'; @@ -17,24 +25,38 @@ describe('group', () => { test('group with no children will be removed automatically', () => { const map = new DocCollection.Y.Map(); - const ids = Array.from({ length: 2 }).map(() => { - const id = service.addElement('shape', { - shapeType: 'rect', - }); - map.set(id, true); + const ids = Array.from({ length: 2 }) + .map(() => { + const id = service.addElement('shape', { + shapeType: 'rect', + }); + map.set(id, true); - return id; - }); + return id; + }) + .concat( + Array.from({ length: 2 }).map(() => { + const id = addNote(doc); + map.set(id, true); + return id; + }) + ); service.addElement('group', { children: map }); + doc.captureSync(); expect(service.elements.length).toBe(3); + service.removeElement(ids[0]); - expect(service.elements.length).toBe(2); - doc.captureSync(); service.removeElement(ids[1]); + doc.captureSync(); + expect(service.elements.length).toBe(1); + + service.removeElement(ids[2]); + service.removeElement(ids[3]); + doc.captureSync(); expect(service.elements.length).toBe(0); doc.undo(); - expect(service.elements.length).toBe(2); + expect(service.elements.length).toBe(1); doc.redo(); expect(service.elements.length).toBe(0); }); @@ -63,4 +85,155 @@ describe('group', () => { expect(doc.getBlock(noteId)).toBeDefined(); expect(service.elements.length).toBe(2); }); + + test("group's xywh should update automatically when children change", async () => { + const shape1 = service.addElement('shape', { + shapeType: 'rect', + xywh: '[0,0,100,100]', + }); + const shape2 = service.addElement('shape', { + shapeType: 'rect', + xywh: '[100,100,100,100]', + }); + const note1 = addNote(doc, { + displayMode: NoteDisplayMode.DocAndEdgeless, + xywh: '[200,200,800,100]', + edgeless: { + style: { + borderRadius: 8, + borderSize: 4, + borderStyle: 'solid', + shadowType: '--affine-note-shadow-box', + }, + collapse: true, + collapsedHeight: 100, + }, + }); + const children = new DocCollection.Y.Map(); + + children.set(shape1, true); + children.set(shape2, true); + children.set(note1, true); + + const groupId = service.addElement('group', { children }); + const group = service.getElementById(groupId) as GroupElementModel; + const assertInitial = () => { + expect(group.x).toBe(0); + expect(group.y).toBe(0); + expect(group.w).toBe(1000); + expect(group.h).toBe(300); + }; + + doc.captureSync(); + await wait(); + assertInitial(); + + service.removeElement(note1); + await wait(); + expect(group.x).toBe(0); + expect(group.y).toBe(0); + expect(group.w).toBe(200); + expect(group.h).toBe(200); + doc.captureSync(); + + doc.undo(); + await wait(); + assertInitial(); + + service.updateElement(note1, { + xywh: '[300,300,800,100]', + }); + await wait(); + expect(group.x).toBe(0); + expect(group.y).toBe(0); + expect(group.w).toBe(1100); + expect(group.h).toBe(400); + doc.captureSync(); + + doc.undo(); + await wait(); + assertInitial(); + + service.removeElement(shape1); + await wait(); + expect(group.x).toBe(100); + expect(group.y).toBe(100); + expect(group.w).toBe(900); + expect(group.h).toBe(200); + doc.captureSync(); + + doc.undo(); + await wait(); + assertInitial(); + + service.updateElement(shape1, { + xywh: '[100,100,100,100]', + }); + await wait(); + expect(group.x).toBe(100); + expect(group.y).toBe(100); + expect(group.w).toBe(900); + expect(group.h).toBe(200); + doc.captureSync(); + + doc.undo(); + await wait(); + assertInitial(); + }); +}); + +describe('mindmap', () => { + let service!: EdgelessRootBlockComponent['service']; + + beforeEach(async () => { + const cleanup = await setupEditor('edgeless'); + service = getDocRootBlock(window.doc, window.editor, 'edgeless').service; + + return cleanup; + }); + + test('delete the root node should remove all children', async () => { + const tree = { + text: 'root', + children: [ + { + text: 'leaf1', + }, + { + text: 'leaf2', + }, + { + text: 'leaf3', + children: [ + { + text: 'leaf4', + }, + ], + }, + ], + }; + const mindmapId = service.addElement('mindmap', { children: tree }); + const mindmap = service.getElementById(mindmapId) as MindmapElementModel; + + expect(service.surface.elementModels.length).toBe(6); + doc.captureSync(); + + service.removeElement(mindmap.tree.element); + expect(service.surface.elementModels.length).toBe(0); + doc.captureSync(); + await wait(); + + doc.undo(); + expect(service.surface.elementModels.length).toBe(6); + await wait(); + + service.removeElement(mindmap.tree.children[2].element); + expect(service.surface.elementModels.length).toBe(4); + doc.captureSync(); + await wait(); + + doc.undo(); + await wait(); + expect(service.surface.elementModels.length).toBe(6); + }); }); diff --git a/packages/presets/src/ai/actions/edgeless-handler.ts b/packages/presets/src/ai/actions/edgeless-handler.ts index f7e112321b76b..38913c0ff412c 100644 --- a/packages/presets/src/ai/actions/edgeless-handler.ts +++ b/packages/presets/src/ai/actions/edgeless-handler.ts @@ -4,6 +4,7 @@ import { ImageBlockModel, MindmapElementModel, NoteBlockModel, + ShapeElementModel, TextElementModel, } from '@blocksuite/blocks'; import { assertExists } from '@blocksuite/global/utils'; @@ -18,6 +19,7 @@ import { createImageRenderer, } from '../messages/wrapper.js'; import { AIProvider } from '../provider.js'; +import { isMindMapRoot } from '../utils/edgeless.js'; import { copyTextAnswer } from '../utils/editor-actions.js'; import { getMarkdownFromSlice } from '../utils/markdown-utils.js'; import { EXCLUDING_COPY_ACTIONS } from './consts.js'; @@ -61,20 +63,23 @@ function actionToRenderer( async function getTextFromSelected(host: EditorHost) { const selected = getCopilotSelectedElems(host); - const { notes, texts } = selected.reduce( + const { notes, texts, shapes } = selected.reduce<{ + notes: NoteBlockModel[]; + texts: TextElementModel[]; + shapes: ShapeElementModel[]; + }>( (pre, cur) => { if (cur instanceof NoteBlockModel) { pre.notes.push(cur); } else if (cur instanceof TextElementModel) { pre.texts.push(cur); + } else if (cur instanceof ShapeElementModel && cur.text) { + pre.shapes.push(cur); } return pre; }, - { notes: [], texts: [] } as { - notes: NoteBlockModel[]; - texts: TextElementModel[]; - } + { notes: [], texts: [], shapes: [] } ); const noteContent = await Promise.all( @@ -86,7 +91,7 @@ async function getTextFromSelected(host: EditorHost) { return `${noteContent.join('\n')} -${texts.map(text => text.text.toString()).join('\n')}`; +${texts.map(text => text.text.toString()).join('\n')}\n${shapes.map(shape => shape.text!.toString())}`; } function actionToStream( @@ -196,7 +201,8 @@ export function actionToHandler( Parameters[0], keyof BlockSuitePresets.AITextActionOptions >, - extract?: (host: EditorHost) => Promise<{ + customInput?: (host: EditorHost) => Promise<{ + input?: string; content?: string; attachments?: string[]; } | void> @@ -205,9 +211,13 @@ export function actionToHandler( const aiPanel = getAIPanel(host); const edgelessCopilot = getEdgelessCopilotWidget(host); let internal: Record = {}; + const selectedElements = getCopilotSelectedElems(host); const ctx = { get() { - return internal; + return { + ...internal, + selectedElements, + }; }, set(data: Record) { internal = data; @@ -223,7 +233,7 @@ export function actionToHandler( aiPanel.config.generateAnswer = actionToGeneration( id, variants, - extract + customInput )(host); aiPanel.config.answerRenderer = actionToRenderer(id, host, ctx); aiPanel.config.finishStateConfig = actionToResponse(id, host, ctx); @@ -259,7 +269,7 @@ export function actionToHandler( }; } -export function noteBlockOrTextShowWen( +export function noteBlockOrTextShowWhen( _: unknown, __: unknown, host: EditorHost @@ -284,7 +294,11 @@ export function noteWithCodeBlockShowWen( export function mindmapShowWhen(_: unknown, __: unknown, host: EditorHost) { const selected = getCopilotSelectedElems(host); - return selected[0] instanceof MindmapElementModel; + return ( + selected.length === 1 && + selected[0]?.group instanceof MindmapElementModel && + !isMindMapRoot(selected[0]) + ); } export function makeItRealShowWhen(_: unknown, __: unknown, host: EditorHost) { @@ -301,3 +315,9 @@ export function explainImageShowWhen( return selected[0] instanceof ImageBlockModel; } + +export function mindmapRootShowWhen(_: unknown, __: unknown, host: EditorHost) { + const selected = getCopilotSelectedElems(host); + + return selected.length === 1 && isMindMapRoot(selected[0]); +} diff --git a/packages/presets/src/ai/actions/edgeless-response.ts b/packages/presets/src/ai/actions/edgeless-response.ts index 43e54f73a684e..71d99010d1b9c 100644 --- a/packages/presets/src/ai/actions/edgeless-response.ts +++ b/packages/presets/src/ai/actions/edgeless-response.ts @@ -16,18 +16,22 @@ import { DeleteIcon, EDGELESS_ELEMENT_TOOLBAR_WIDGET, EmbedHtmlBlockSpec, + fitContent, InsertBelowIcon, NoteDisplayMode, ResetIcon, - updateMindmapNodeRect, } from '@blocksuite/blocks'; import { insertFromMarkdown } from '../_common/markdown-utils.js'; import { getSurfaceElementFromEditor } from '../_common/selection-utils.js'; import { getAIPanel } from '../ai-panel.js'; +import { isMindMapRoot } from '../utils/edgeless.js'; import { preprocessHtml } from '../utils/html.js'; import { fetchImageToFile } from '../utils/image.js'; -import { getEdgelessRootFromEditor } from '../utils/selection-utils.js'; +import { + getEdgelessRootFromEditor, + getEdgelessService, +} from '../utils/selection-utils.js'; export type CtxRecord = { get(): Record; @@ -71,6 +75,12 @@ export function getElementToolbar( return elementToolbar; } +export function getTriggerEntry(host: EditorHost) { + const copilotWidget = getEdgelessCopilotWidget(host); + + return copilotWidget.visible ? 'selection' : 'toolbar'; +} + export function getCopilotSelectedElems(host: EditorHost): EdgelessModel[] { const service = getService(host); const copilogWidget = getEdgelessCopilotWidget(host); @@ -125,22 +135,82 @@ export function createInsertResp( }; } +type MindMapNode = { + text: string; + children: MindMapNode[]; +}; + export const responses: { [key in keyof Partial]: ( host: EditorHost, ctx: CtxRecord ) => void; } = { + expandMindmap: (host, ctx) => { + const aiPanel = getAIPanel(host); + const [surface] = host.doc.getBlockByFlavour( + 'affine:surface' + ) as SurfaceBlockModel[]; + + const elements = ctx.get()['selectedElements'] as EdgelessModel[]; + const data = ctx.get() as { + node: MindMapNode; + }; + + aiPanel.hide(); + + const mindmap = elements[0].group as MindmapElementModel; + + if (!data?.node) return; + + if (data.node.children) { + data.node.children.forEach(childTree => { + mindmap.addTree(elements[0].id, childTree); + }); + + const subtree = mindmap.getNode(elements[0].id); + + if (!subtree) return; + + surface.doc.transact(() => { + const updateNodeSize = (node: typeof subtree) => { + fitContent(node.element as ShapeElementModel); + + node.children.forEach(child => { + updateNodeSize(child); + }); + }; + + updateNodeSize(subtree); + }); + } + }, brainstormMindmap: (host, ctx) => { const aiPanel = getAIPanel(host); + const edgelessService = getEdgelessService(host); const edgelessCopilot = getEdgelessCopilotWidget(host); + const selectionRect = edgelessCopilot.selectionModelRect; const [surface] = host.doc.getBlockByFlavour( 'affine:surface' ) as SurfaceBlockModel[]; - + const elements = ctx.get()['selectedElements'] as EdgelessModel[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any const data = ctx.get() as any; - const selectionRect = edgelessCopilot.selectionModelRect; + let needTomoveMindMap = true; + let focus = false; + + if (isMindMapRoot(elements[0])) { + const mindmap = elements[0].group as MindmapElementModel; + const xywh = mindmap.tree.element.xywh; + + surface.removeElement(mindmap.id); + + if (data.node) { + data.node.xywh = xywh; + needTomoveMindMap = false; + focus = true; + } + } edgelessCopilot.hideCopilotPanel(); aiPanel.hide(); @@ -155,15 +225,13 @@ export const responses: { host.doc.transact(() => { const rootElement = mindmap.tree.element; - if (selectionRect) { - rootElement.xywh = `[${selectionRect.x},${selectionRect.y},${rootElement.w},${rootElement.h}]`; - } - mindmap.childElements.forEach(shape => { - updateMindmapNodeRect(shape as ShapeElementModel); + fitContent(shape as ShapeElementModel); }); - if (selectionRect) { + if (selectionRect && needTomoveMindMap) { + rootElement.xywh = `[${selectionRect.x},${selectionRect.y},${rootElement.w},${rootElement.h}]`; + queueMicrotask(() => { mindmap.moveTo([ selectionRect.x, @@ -173,6 +241,15 @@ export const responses: { ]); }); } + + if (focus) { + queueMicrotask(() => { + edgelessService.selection.set({ + elements: [mindmap.tree.element.id], + editing: false, + }); + }); + } }); }, makeItReal: host => { @@ -292,7 +369,7 @@ const defaultHandler = (host: EditorHost) => { }); }; -export function getResponseHandler( +export function getInsertHandler( id: T, host: EditorHost, ctx: CtxRecord @@ -312,7 +389,7 @@ export function actionToResponse( { name: 'Response', items: [ - getResponseHandler(id, host, ctx), + getInsertHandler(id, host, ctx), retry(getAIPanel(host)), discard(getAIPanel(host), getEdgelessCopilotWidget(host)), ], diff --git a/packages/presets/src/ai/actions/types.ts b/packages/presets/src/ai/actions/types.ts index 8fc0b63deb15c..e1d57000db587 100644 --- a/packages/presets/src/ai/actions/types.ts +++ b/packages/presets/src/ai/actions/types.ts @@ -50,6 +50,10 @@ declare global { tone: (typeof textTones)[number]; } + interface ExpandMindMap extends AITextActionOptions { + mindmap: string; + } + interface AIActions { // chat is a bit special because it's has a internally maintained session chat(options: T): AIActionTextResponse; @@ -112,7 +116,7 @@ declare global { brainstormMindmap( options: T ): AIActionTextResponse; - expandMindmap( + expandMindmap( options: T ): AIActionTextResponse; diff --git a/packages/presets/src/ai/entries/edgeless/actions-config.ts b/packages/presets/src/ai/entries/edgeless/actions-config.ts index 9668da4a7ff93..f935eb4b8b384 100644 --- a/packages/presets/src/ai/entries/edgeless/actions-config.ts +++ b/packages/presets/src/ai/entries/edgeless/actions-config.ts @@ -1,8 +1,9 @@ -import type { AIItemGroupConfig } from '@blocksuite/blocks'; +import type { AIItemGroupConfig, ShapeElementModel } from '@blocksuite/blocks'; import { AIPenIcon, BlocksUtils, LanguageIcon, + MindmapElementModel, TextElementModel, } from '@blocksuite/blocks'; @@ -10,12 +11,14 @@ import { actionToHandler, explainImageShowWhen, makeItRealShowWhen, + mindmapRootShowWhen, mindmapShowWhen, - noteBlockOrTextShowWen, + noteBlockOrTextShowWhen, } from '../../actions/edgeless-handler.js'; import { getCopilotSelectedElems } from '../../actions/edgeless-response.js'; import { translateLangs } from '../../actions/types.js'; import { getAIPanel } from '../../ai-panel.js'; +import { mindMapToMarkdown } from '../../utils/edgeless.js'; import { getEdgelessRootFromEditor } from '../../utils/selection-utils.js'; const translateSubItem = translateLangs.map(lang => { @@ -31,7 +34,7 @@ export const docGroup: AIItemGroupConfig = { { name: 'Summary', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('summary'), }, ], @@ -43,7 +46,7 @@ export const othersGroup: AIItemGroupConfig = { { name: 'Find actions from it', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('findActions'), }, { @@ -61,37 +64,37 @@ export const editGroup: AIItemGroupConfig = { { name: 'Translate to', icon: LanguageIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, subItem: translateSubItem, }, { name: 'Improve writing for it', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('improveWriting'), }, { name: 'Improve grammar for it', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('improveGrammar'), }, { name: 'Fix spelling ', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('fixSpelling'), }, { name: 'Make longer', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('makeLonger'), }, { name: 'Make shorter', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('makeShorter'), }, ], @@ -103,37 +106,37 @@ export const draftGroup: AIItemGroupConfig = { { name: 'Write an article about this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('writeArticle'), }, { name: 'Write a tweet about this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('writeTwitterPost'), }, { name: 'Write a poem about this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('writePoem'), }, { name: 'Write a blog post about this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('writeBlogPost'), }, { name: 'Write a outline from this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('writeOutline'), }, { name: 'Brainstorm ideas about this', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('brainstorm'), }, ], @@ -143,15 +146,34 @@ export const mindmapGroup: AIItemGroupConfig = { name: 'mindmap with ai', items: [ { - name: 'Expand from this mindmap node', + name: 'Expand from this mind map node', icon: AIPenIcon, showWhen: mindmapShowWhen, - handler: actionToHandler('expandMindmap'), + handler: actionToHandler('expandMindmap', undefined, function (host) { + const selected = getCopilotSelectedElems(host); + const firstSelected = selected[0] as ShapeElementModel; + const mindmap = firstSelected?.group; + + if (!(mindmap instanceof MindmapElementModel)) { + return Promise.resolve({}); + } + + return Promise.resolve({ + input: firstSelected.text?.toString() ?? '', + content: mindMapToMarkdown(mindmap), + }); + }), + }, + { + name: 'Brainstorm ideas with mind map', + icon: AIPenIcon, + showWhen: noteBlockOrTextShowWhen, + handler: actionToHandler('brainstormMindmap'), }, { - name: 'Brainstorm ideas with Mindmap', + name: 'Regenerate mind map', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: mindmapRootShowWhen, handler: actionToHandler('brainstormMindmap'), }, ], @@ -163,7 +185,7 @@ export const presentationGroup: AIItemGroupConfig = { { name: 'Create a presentation', icon: AIPenIcon, - showWhen: noteBlockOrTextShowWen, + showWhen: noteBlockOrTextShowWhen, handler: actionToHandler('createSlides'), }, ], diff --git a/packages/presets/src/ai/utils/edgeless.ts b/packages/presets/src/ai/utils/edgeless.ts new file mode 100644 index 0000000000000..3aeb760f3ed9b --- /dev/null +++ b/packages/presets/src/ai/utils/edgeless.ts @@ -0,0 +1,34 @@ +import { + type EdgelessModel, + MindmapElementModel, + type ShapeElementModel, +} from '@blocksuite/blocks'; + +export function mindMapToMarkdown(mindmap: MindmapElementModel) { + let markdownStr = ''; + + const traverse = ( + node: MindmapElementModel['tree']['children'][number], + indent: number = 0 + ) => { + markdownStr += `${node.element as ShapeElementModel}\n`; + + const text = (node.element as ShapeElementModel).text?.toString() ?? ''; + + markdownStr += `${' '.repeat(indent)}${text}\n`; + + if (node.children) { + node.children.forEach(traverse, indent + 1); + } + }; + + traverse(mindmap.tree, 0); + + return markdownStr; +} + +export function isMindMapRoot(ele: EdgelessModel) { + const group = ele?.group; + + return group instanceof MindmapElementModel && group.tree.element === ele; +} From 088776c5f105bd62c6387453dd098d50ebdacaa0 Mon Sep 17 00:00:00 2001 From: donteatfriedrice Date: Wed, 24 Apr 2024 07:40:30 +0000 Subject: [PATCH 2/2] fix(blocks): github icon dark mode (#6864) Screenshot 2024-04-24 at 15 21 13 Screenshot 2024-04-24 at 15 21 30 --- .../blocks/src/embed-github-block/styles.ts | 27 ++++++++++++------- .../components/toolbar/note/note-menu.ts | 3 +++ .../root-block/widgets/slash-menu/styles.ts | 3 +++ 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/blocks/src/embed-github-block/styles.ts b/packages/blocks/src/embed-github-block/styles.ts index 0fd64951cbc2d..b6ab0cb668567 100644 --- a/packages/blocks/src/embed-github-block/styles.ts +++ b/packages/blocks/src/embed-github-block/styles.ts @@ -32,6 +32,7 @@ export const styles = css` .affine-embed-github-content-title { display: flex; + min-height: 22px; flex-direction: row; gap: 8px; align-items: center; @@ -54,7 +55,7 @@ export const styles = css` .affine-embed-github-content-title-icons svg { width: 16px; height: 16px; - fill: var(--affine-background-primary-color); + color: var(--affine-pure-white); } .affine-embed-github-content-title-site-icon { @@ -63,6 +64,11 @@ export const styles = css` height: 16px; justify-content: center; align-items: center; + + .github-icon { + fill: var(--affine-black); + color: var(--affine-black); + } } .affine-embed-github-content-title-status-icon { @@ -126,7 +132,7 @@ export const styles = css` word-break: break-word; overflow: hidden; text-overflow: ellipsis; - color: var(--affine-text-primary-color); + color: var(--affine-pure-white); font-family: var(--affine-font-family); font-size: var(--affine-font-sm); @@ -386,17 +392,17 @@ export const styles = css` `; export const GithubIcon = html` `; @@ -404,7 +410,7 @@ export const GithubIssueOpenIcon = html` @@ -413,11 +419,6 @@ export const GithubIssueOpenIcon = html` `; -// -// -// -// - export const GithubIssueClosedSuccessIcon = html`