mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 08:50:50 +08:00
fix(core): in edgeless mode, an error occurs when asking AI questions without selecting any content (#12437)
### TL;DR fix: in edgeless mode, an error occurs when asking AI questions without selecting any content > CLOSE AI-133 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for asking AI input in edgeless mode when no content is selected. - Enhanced AI panel behavior with improved input handling and chat message sending in edgeless mode. - **Bug Fixes** - Improved handling of AI chat input visibility and context extraction in edgeless mode. - **Tests** - Introduced new end-to-end tests to verify chat interactions with AI in edgeless mode. - Centralized editor content removal logic in test utilities for better maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -6,14 +6,9 @@ import {
|
|||||||
type ShapeElementModel,
|
type ShapeElementModel,
|
||||||
} from '@blocksuite/affine/model';
|
} from '@blocksuite/affine/model';
|
||||||
import { matchModels } from '@blocksuite/affine/shared/utils';
|
import { matchModels } from '@blocksuite/affine/shared/utils';
|
||||||
import type { BlockComponent, EditorHost } from '@blocksuite/affine/std';
|
import type { BlockComponent } from '@blocksuite/affine/std';
|
||||||
import type { GfxModel } from '@blocksuite/affine/std/gfx';
|
import type { GfxModel } from '@blocksuite/affine/std/gfx';
|
||||||
|
|
||||||
import {
|
|
||||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
|
||||||
type EdgelessCopilotWidget,
|
|
||||||
} from '../widgets/edgeless-copilot';
|
|
||||||
|
|
||||||
export function mindMapToMarkdown(mindmap: MindmapElementModel) {
|
export function mindMapToMarkdown(mindmap: MindmapElementModel) {
|
||||||
let markdownStr = '';
|
let markdownStr = '';
|
||||||
|
|
||||||
@@ -45,17 +40,7 @@ export function isMindmapChild(ele: GfxModel) {
|
|||||||
return ele?.group instanceof MindmapElementModel && !isMindMapRoot(ele);
|
return ele?.group instanceof MindmapElementModel && !isMindMapRoot(ele);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEdgelessCopilotWidget(
|
export { getEdgelessCopilotWidget } from './get-edgeless-copilot-widget';
|
||||||
host: EditorHost
|
|
||||||
): EdgelessCopilotWidget {
|
|
||||||
const rootBlockId = host.store.root?.id as string;
|
|
||||||
const copilotWidget = host.view.getWidget(
|
|
||||||
AFFINE_EDGELESS_COPILOT_WIDGET,
|
|
||||||
rootBlockId
|
|
||||||
) as EdgelessCopilotWidget;
|
|
||||||
|
|
||||||
return copilotWidget;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findNoteBlockModel(blockElement: BlockComponent) {
|
export function findNoteBlockModel(blockElement: BlockComponent) {
|
||||||
let curBlock = blockElement;
|
let curBlock = blockElement;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import type { EditorHost } from '@blocksuite/affine/std';
|
||||||
|
|
||||||
|
import type { EdgelessCopilotWidget } from '../widgets/edgeless-copilot';
|
||||||
|
import { AFFINE_EDGELESS_COPILOT_WIDGET } from '../widgets/edgeless-copilot/constant';
|
||||||
|
|
||||||
|
export function getEdgelessCopilotWidget(
|
||||||
|
host: EditorHost
|
||||||
|
): EdgelessCopilotWidget {
|
||||||
|
const rootBlockId = host.store.root?.id as string;
|
||||||
|
const copilotWidget = host.view.getWidget(
|
||||||
|
AFFINE_EDGELESS_COPILOT_WIDGET,
|
||||||
|
rootBlockId
|
||||||
|
) as EdgelessCopilotWidget;
|
||||||
|
|
||||||
|
return copilotWidget;
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ import {
|
|||||||
|
|
||||||
import { getContentFromSlice } from '../../utils';
|
import { getContentFromSlice } from '../../utils';
|
||||||
import type { CopilotTool } from '../tool/copilot-tool';
|
import type { CopilotTool } from '../tool/copilot-tool';
|
||||||
import { getEdgelessCopilotWidget } from './edgeless';
|
import { getEdgelessCopilotWidget } from './get-edgeless-copilot-widget';
|
||||||
|
|
||||||
export async function selectedToCanvas(host: EditorHost) {
|
export async function selectedToCanvas(host: EditorHost) {
|
||||||
const gfx = host.std.get(GfxControllerIdentifier);
|
const gfx = host.std.get(GfxControllerIdentifier);
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const AFFINE_EDGELESS_COPILOT_WIDGET = 'affine-edgeless-copilot-widget';
|
||||||
@@ -27,13 +27,14 @@ import { styleMap } from 'lit/directives/style-map.js';
|
|||||||
import { literal, unsafeStatic } from 'lit/static-html.js';
|
import { literal, unsafeStatic } from 'lit/static-html.js';
|
||||||
|
|
||||||
import type { AIItemGroupConfig } from '../../components/ai-item/types.js';
|
import type { AIItemGroupConfig } from '../../components/ai-item/types.js';
|
||||||
|
import { AIProvider } from '../../provider/index.js';
|
||||||
|
import { extractSelectedContent } from '../../utils/extract.js';
|
||||||
import {
|
import {
|
||||||
AFFINE_AI_PANEL_WIDGET,
|
AFFINE_AI_PANEL_WIDGET,
|
||||||
AffineAIPanelWidget,
|
AffineAIPanelWidget,
|
||||||
} from '../ai-panel/ai-panel.js';
|
} from '../ai-panel/ai-panel.js';
|
||||||
import { EdgelessCopilotPanel } from '../edgeless-copilot-panel/index.js';
|
import { EdgelessCopilotPanel } from '../edgeless-copilot-panel/index.js';
|
||||||
|
import { AFFINE_EDGELESS_COPILOT_WIDGET } from './constant.js';
|
||||||
export const AFFINE_EDGELESS_COPILOT_WIDGET = 'affine-edgeless-copilot-widget';
|
|
||||||
|
|
||||||
export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
||||||
static override styles = css`
|
static override styles = css`
|
||||||
@@ -95,6 +96,31 @@ export class EdgelessCopilotWidget extends WidgetComponent<RootBlockModel> {
|
|||||||
|
|
||||||
if (input instanceof AffineAIPanelWidget) {
|
if (input instanceof AffineAIPanelWidget) {
|
||||||
input.setState('input', referenceElement);
|
input.setState('input', referenceElement);
|
||||||
|
const aiPanel = input;
|
||||||
|
// TODO: @xiaojun refactor these scattered config overrides
|
||||||
|
if (aiPanel.config && !aiPanel.config.generateAnswer) {
|
||||||
|
aiPanel.config.generateAnswer = ({ finish, input }) => {
|
||||||
|
finish('success');
|
||||||
|
aiPanel.hide();
|
||||||
|
extractSelectedContent(this.host)
|
||||||
|
.then(context => {
|
||||||
|
AIProvider.slots.requestSendWithChat.next({
|
||||||
|
input,
|
||||||
|
context,
|
||||||
|
host: this.host,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
};
|
||||||
|
aiPanel.config.inputCallback = text => {
|
||||||
|
const panel = this.shadowRoot?.querySelector(
|
||||||
|
'edgeless-copilot-panel'
|
||||||
|
);
|
||||||
|
if (panel instanceof HTMLElement) {
|
||||||
|
panel.style.visibility = text ? 'hidden' : 'visible';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this._createCopilotPanel();
|
this._createCopilotPanel();
|
||||||
this._updateCopilotPanel(input);
|
this._updateCopilotPanel(input);
|
||||||
@@ -314,3 +340,5 @@ declare global {
|
|||||||
[AFFINE_EDGELESS_COPILOT_WIDGET]: EdgelessCopilotWidget;
|
[AFFINE_EDGELESS_COPILOT_WIDGET]: EdgelessCopilotWidget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export * from './constant';
|
||||||
|
|||||||
@@ -484,4 +484,31 @@ test.describe('AIBasic/Chat', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should support chat with ask ai input in edgeless mode when nothing selected', async ({
|
||||||
|
loggedInPage: page,
|
||||||
|
utils,
|
||||||
|
}) => {
|
||||||
|
await utils.chatPanel.closeChatPanel(page);
|
||||||
|
await utils.editor.switchToEdgelessMode(page);
|
||||||
|
await utils.editor.removeAll(page);
|
||||||
|
|
||||||
|
await page.mouse.move(300, 300);
|
||||||
|
await page.mouse.down({ button: 'right' });
|
||||||
|
await page.mouse.move(350, 350);
|
||||||
|
await page.mouse.up({ button: 'right' });
|
||||||
|
|
||||||
|
await page.keyboard.type('Who are you?');
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await utils.chatPanel.waitForHistory(page, [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: 'Who are you?',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
status: 'success',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -386,14 +386,18 @@ export class EditorUtils {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async removeAll(page: Page) {
|
||||||
|
await selectAllByKeyboard(page);
|
||||||
|
await page.keyboard.press('Delete');
|
||||||
|
}
|
||||||
|
|
||||||
public static async askAIWithEdgeless(
|
public static async askAIWithEdgeless(
|
||||||
page: Page,
|
page: Page,
|
||||||
createBlock: () => Promise<void>,
|
createBlock: () => Promise<void>,
|
||||||
afterSelected?: () => Promise<void>
|
afterSelected?: () => Promise<void>
|
||||||
) {
|
) {
|
||||||
await this.switchToEdgelessMode(page);
|
await this.switchToEdgelessMode(page);
|
||||||
await selectAllByKeyboard(page);
|
await this.removeAll(page);
|
||||||
await page.keyboard.press('Delete');
|
|
||||||
await createBlock();
|
await createBlock();
|
||||||
await pressEscape(page, 5);
|
await pressEscape(page, 5);
|
||||||
await selectAllByKeyboard(page);
|
await selectAllByKeyboard(page);
|
||||||
|
|||||||
Reference in New Issue
Block a user