L-Sun
2025-01-21 16:00:49 +00:00
parent d4aeac5d35
commit 29995e498a
4 changed files with 102 additions and 11 deletions
@@ -117,14 +117,16 @@ export class PageRootBlockComponent extends BlockComponent<
/**
* Focus the first paragraph in the default note block.
* If there is no paragraph, create one.
* @return { id: string, created: boolean } id of the focused paragraph and whether it is created or not
*/
focusFirstParagraph = () => {
focusFirstParagraph = (): { id: string; created: boolean } => {
const defaultNote = this._getDefaultNoteBlock();
const firstText = defaultNote?.children.find(block =>
matchFlavours(block, ['affine:paragraph', 'affine:list', 'affine:code'])
);
if (firstText) {
focusTextModel(this.std, firstText.id);
return { id: firstText.id, created: false };
} else {
const newFirstParagraphId = this.doc.addBlock(
'affine:paragraph',
@@ -133,6 +135,7 @@ export class PageRootBlockComponent extends BlockComponent<
0
);
focusTextModel(this.std, newFirstParagraphId);
return { id: newFirstParagraphId, created: true };
}
};
@@ -1,11 +1,14 @@
import { type EditorHost, TextSelection } from '@blocksuite/affine/block-std';
import type {
AffineAIPanelWidget,
AffineAIPanelWidgetConfig,
AIError,
import {
type AffineAIPanelWidget,
type AffineAIPanelWidgetConfig,
type AIError,
type AIItemGroupConfig,
createLitPortal,
} from '@blocksuite/affine/blocks';
import { assertExists } from '@blocksuite/affine/global/utils';
import type { TemplateResult } from 'lit';
import { flip, offset } from '@floating-ui/dom';
import { html, type TemplateResult } from 'lit';
import {
buildCopyConfig,
@@ -207,7 +210,10 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
};
}
export function handleInlineAskAIAction(host: EditorHost) {
export function handleInlineAskAIAction(
host: EditorHost,
actionGroups?: AIItemGroupConfig[]
) {
const panel = getAIPanelWidget(host);
const selection = host.selection.find(TextSelection);
const lastBlockPath = selection
@@ -216,6 +222,7 @@ export function handleInlineAskAIAction(host: EditorHost) {
if (!lastBlockPath) return;
const block = host.view.getBlock(lastBlockPath);
if (!block) return;
const generateAnswer: AffineAIPanelWidgetConfig['generateAnswer'] = ({
finish,
input,
@@ -244,7 +251,54 @@ export function handleInlineAskAIAction(host: EditorHost) {
})
.catch(console.error);
};
assertExists(panel.config);
if (!panel.config) return;
panel.config.generateAnswer = generateAnswer;
if (!actionGroups) {
panel.toggle(block);
return;
}
let actionPanel: HTMLDivElement | null = null;
let abortController: AbortController | null = null;
const clear = () => {
abortController?.abort();
actionPanel = null;
abortController = null;
};
panel.config.inputCallback = text => {
if (!actionPanel) return;
actionPanel.style.visibility = text ? 'hidden' : 'visible';
};
panel.config.hideCallback = () => {
clear();
};
panel.toggle(block);
setTimeout(() => {
abortController = new AbortController();
actionPanel = createLitPortal({
template: html`
<ask-ai-panel
.host=${host}
.actionGroups=${actionGroups}
.onItemClick=${() => {
panel.restoreSelection();
clear();
}}
></ask-ai-panel>
`,
computePosition: {
referenceElement: panel,
placement: 'top-start',
middleware: [flip(), offset({ mainAxis: 3 })],
autoUpdate: true,
},
abortController: abortController,
closeOnClickAway: true,
});
}, 0);
}
@@ -1,3 +1,5 @@
import { handleInlineAskAIAction } from '@affine/core/blocksuite/presets/ai';
import { pageAIGroups } from '@affine/core/blocksuite/presets/ai/_common/config';
import { DocsService } from '@affine/core/modules/doc';
import { EditorService } from '@affine/core/modules/editor';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
@@ -5,6 +7,7 @@ import { TemplateDocService } from '@affine/core/modules/template-doc';
import { TemplateListMenu } from '@affine/core/modules/template-doc/view/template-list-menu';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
import { PageRootBlockComponent } from '@blocksuite/affine/blocks';
import type { Store } from '@blocksuite/affine/store';
import {
AiIcon,
@@ -62,6 +65,7 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
[doc.id, templateDocService.list]
)
);
const enableAI = useLiveData(featureFlagService.flags.enable_ai.$);
const enableTemplateDoc = useLiveData(
featureFlagService.flags.enable_template_doc.$
);
@@ -85,10 +89,29 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
setTemplateMenuOpen(open);
}, []);
const showAI = false;
const startWithAI = useCallback(() => {
const std = editorService.editor.editorContainer$.value?.std;
if (!std) return;
const rootBlockId = std.host.doc.root?.id;
if (!rootBlockId) return;
const rootComponent = std.view.getBlock(rootBlockId);
if (!(rootComponent instanceof PageRootBlockComponent)) return;
const { id, created } = rootComponent.focusFirstParagraph();
if (created) {
std.view.viewUpdated.once(v => {
if (v.id === id) handleInlineAskAIAction(std.host, pageAIGroups);
});
} else {
handleInlineAskAIAction(std.host, pageAIGroups);
}
}, [editorService.editor]);
const showTemplate = !isTemplate && enableTemplateDoc;
if (!showAI && !showTemplate) {
if (!enableAI && !showTemplate) {
return null;
}
@@ -96,10 +119,12 @@ const StarterBarNotEmpty = ({ doc }: { doc: Store }) => {
<div className={styles.root}>
{t['com.affine.page-starter-bar.start']()}
<ul className={styles.badges}>
{showAI ? (
{enableAI ? (
<Badge
data-testid="start-with-ai-badge"
icon={<AiIcon />}
text={t['com.affine.page-starter-bar.ai']()}
onClick={startWithAI}
/>
) : null}
@@ -431,6 +431,15 @@ test.describe('chat panel', () => {
expect(history[1].name).toBe('AFFiNE AI');
expect(await page.locator('chat-panel affine-link').count()).toBe(0);
});
test('can trigger inline ai input and action panel by clicking Start with AI button', async ({
page,
}) => {
await clickNewPageButton(page);
await page.getByTestId('start-with-ai-badge').click();
await expect(page.locator('affine-ai-panel-widget')).toBeVisible();
await expect(page.locator('ask-ai-panel')).toBeVisible();
});
});
test.describe('chat with block', () => {