diff --git a/packages/frontend/core/src/blocksuite/presets/ai/peek-view/chat-block-peek-view.ts b/packages/frontend/core/src/blocksuite/presets/ai/peek-view/chat-block-peek-view.ts index a3e792088..8a76620c6 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/peek-view/chat-block-peek-view.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/peek-view/chat-block-peek-view.ts @@ -10,7 +10,6 @@ import { ConnectorMode, type EdgelessRootService, } from '@blocksuite/blocks'; -import { Bound } from '@blocksuite/global/utils'; import { type AIChatBlockModel, type ChatMessage, @@ -32,6 +31,7 @@ import { AIChatErrorRenderer } from '../messages/error'; import { AIProvider } from '../provider'; import { PeekViewStyles } from './styles'; import type { ChatContext } from './types'; +import { calcChildBound } from './utils'; @customElement('ai-chat-block-peek-view') export class AIChatBlockPeekView extends LitElement { @@ -53,10 +53,6 @@ export class AIChatBlockPeekView extends LitElement { return this.parentModel.messages; } - private get parentXYWH() { - return this.parentModel.xywh; - } - private get parentChatBlockId() { return this.parentModel.id; } @@ -156,21 +152,6 @@ export class AIChatBlockPeekView extends LitElement { return; } - const parentXYWH = Bound.deserialize(this.parentXYWH); - const { - x: parentX, - y: parentY, - w: parentWidth, - h: parentHeight, - } = parentXYWH; - - // Add AI chat block to the center of the viewport - // TODO: optimize the position of the AI chat block - const gap = parentWidth; - const x = parentX + parentWidth + gap; - const y = parentY; - const bound = new Bound(x, y, parentWidth, parentHeight); - // Get fork session messages const messages = await this._constructBranchChatBlockMessages( doc, @@ -181,6 +162,7 @@ export class AIChatBlockPeekView extends LitElement { } const edgelessService = this._rootService as EdgelessRootService; + const bound = calcChildBound(this.parentModel, edgelessService); const aiChatBlockId = edgelessService.addBlock( 'affine:embed-ai-chat' as keyof BlockSuite.BlockModels, { diff --git a/packages/frontend/core/src/blocksuite/presets/ai/peek-view/utils.ts b/packages/frontend/core/src/blocksuite/presets/ai/peek-view/utils.ts new file mode 100644 index 000000000..8f68cc7dd --- /dev/null +++ b/packages/frontend/core/src/blocksuite/presets/ai/peek-view/utils.ts @@ -0,0 +1,84 @@ +import type { EdgelessRootService } from '@blocksuite/blocks'; +import { Bound } from '@blocksuite/global/utils'; +import { + type AIChatBlockModel, + CHAT_BLOCK_HEIGHT, + CHAT_BLOCK_WIDTH, +} from '@blocksuite/presets'; + +/** + * Calculates the bounding box for a child block + * Based on the parent block's position and the existing connectors. + * Place the child block to the right of the parent block as much as possible. + * If the parent block already has connected child blocks + * Distribute them evenly along the y-axis as much as possible. + * @param parentModel - The model of the parent block. + * @param service - The EdgelessRootService instance. + * @returns The calculated bounding box for the child block. + */ +export function calcChildBound( + parentModel: AIChatBlockModel, + service: EdgelessRootService +) { + const parentXYWH = Bound.deserialize(parentModel.xywh); + const { x: parentX, y: parentY, w: parentWidth } = parentXYWH; + + const connectors = service.getConnectors(parentModel.id); + const gapX = CHAT_BLOCK_WIDTH; + const gapY = 60; + const defaultX = parentX + parentWidth + gapX; + const defaultY = parentY; + + if (!connectors.length) { + return new Bound(defaultX, defaultY, CHAT_BLOCK_WIDTH, CHAT_BLOCK_HEIGHT); + } else { + // Filter out the connectors which source is the parent block + const childConnectors = connectors.filter( + connector => connector.source.id === parentModel.id + ); + // Get all the target blocks of the child connectors + const targetBlocks = childConnectors + .map(connector => connector.target.id) + .filter(id => id !== undefined) + .map(id => service.getElementById(id)) + .filter(block => !!block); + + if (targetBlocks.length) { + // Sort target blocks by their y position + targetBlocks.sort( + (a, b) => Bound.deserialize(a.xywh).y - Bound.deserialize(b.xywh).y + ); + + let x, y; + // Calculate the position based on the number of target blocks + const middleIndex = Math.floor((targetBlocks.length - 1) / 2); + const middleBlock = targetBlocks[middleIndex]; + const { y: middleY, h: middleHeight } = Bound.deserialize( + middleBlock.xywh + ); + const lastBlock = targetBlocks[targetBlocks.length - 1]; + const { + x: lastX, + y: lastY, + h: lastHeight, + } = Bound.deserialize(lastBlock.xywh); + + if (targetBlocks.length % 2 === 0) { + // If even number of target blocks + // place the new bound above the middle block with same same gap as the last block + x = lastX; + const gap = lastY - (middleY + middleHeight); + y = middleY - gap - CHAT_BLOCK_HEIGHT; + } else { + // If odd number of target blocks, place the new bound below the last block with a gap + x = lastX; + y = lastY + lastHeight + gapY; + } + + return new Bound(x, y, CHAT_BLOCK_WIDTH, CHAT_BLOCK_HEIGHT); + } else { + // If no valid target blocks, fallback to default position + return new Bound(defaultX, defaultY, CHAT_BLOCK_WIDTH, CHAT_BLOCK_HEIGHT); + } + } +}