diff --git a/blocksuite/affine/block-root/src/widgets/ai-panel/ai-panel.ts b/blocksuite/affine/block-root/src/widgets/ai-panel/ai-panel.ts index 2fbd034510..21a2ea1a8e 100644 --- a/blocksuite/affine/block-root/src/widgets/ai-panel/ai-panel.ts +++ b/blocksuite/affine/block-root/src/widgets/ai-panel/ai-panel.ts @@ -278,7 +278,7 @@ export class AffineAIPanelWidget extends WidgetComponent { input?: string, shouldTriggerCallback?: boolean ) => { - if (input) { + if (typeof input === 'string') { this._inputText = input; this.generate(); } else { diff --git a/packages/frontend/core/src/blocksuite/presets/ai/actions/doc-handler.ts b/packages/frontend/core/src/blocksuite/presets/ai/actions/doc-handler.ts index e437cf7456..5e1db00b23 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/actions/doc-handler.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/actions/doc-handler.ts @@ -1,9 +1,9 @@ import { type EditorHost, TextSelection } from '@blocksuite/affine/block-std'; import { type AffineAIPanelWidget, - type AffineAIPanelWidgetConfig, type AIError, type AIItemGroupConfig, + AIStarIconWithAnimation, createLitPortal, } from '@blocksuite/affine/blocks'; import { assertExists } from '@blocksuite/affine/global/utils'; @@ -64,66 +64,63 @@ export function bindTextStream( }); } -export function actionToStream( +function actionToStream( + host: EditorHost, id: T, + input: string, signal?: AbortSignal, variants?: Omit< Parameters[0], keyof BlockSuitePresets.AITextActionOptions >, trackerOptions?: BlockSuitePresets.TrackerOptions -) { +): BlockSuitePresets.TextStream | undefined { const action = AIProvider.actions[id]; if (!action || typeof action !== 'function') return; - return (host: EditorHost): BlockSuitePresets.TextStream => { - let stream: BlockSuitePresets.TextStream | undefined; - return { - async *[Symbol.asyncIterator]() { - const { currentTextSelection, selectedBlocks } = getSelections(host); - let markdown: string; - let attachments: File[] = []; + let stream: BlockSuitePresets.TextStream | undefined; + return { + async *[Symbol.asyncIterator]() { + const { currentTextSelection, selectedBlocks } = getSelections(host); - if (currentTextSelection?.isCollapsed()) { - markdown = await selectAboveBlocks(host); - } else { - [markdown, attachments] = await Promise.all([ - getSelectedTextContent(host), - getSelectedImagesAsBlobs(host), - ]); - } + let markdown: string; + let attachments: File[] = []; - // for now if there are more than one selected blocks, we will not omit the attachments - const sendAttachments = - selectedBlocks?.length === 1 && attachments.length > 0; - const models = selectedBlocks?.map(block => block.model); - const control = trackerOptions?.control ?? 'format-bar'; - const where = trackerOptions?.where ?? 'ai-panel'; - const options = { - ...variants, - attachments: sendAttachments ? attachments : undefined, - input: sendAttachments ? '' : markdown, - stream: true, - host, - models, - signal, - control, - where, - docId: host.doc.id, - workspaceId: host.doc.workspace.id, - } as Parameters[0]; - // @ts-expect-error TODO(@Peng): maybe fix this - stream = action(options); - if (!stream) return; - yield* stream; - }, - }; + if (currentTextSelection?.isCollapsed()) { + markdown = await selectAboveBlocks(host); + } else { + [markdown, attachments] = await Promise.all([ + getSelectedTextContent(host), + getSelectedImagesAsBlobs(host), + ]); + } + + const models = selectedBlocks?.map(block => block.model); + const control = trackerOptions?.control ?? 'format-bar'; + const where = trackerOptions?.where ?? 'ai-panel'; + const options = { + ...variants, + attachments, + input: input ? `${markdown}\n${input}` : markdown, + stream: true, + host, + models, + signal, + control, + where, + docId: host.doc.id, + workspaceId: host.doc.workspace.id, + } as Parameters[0]; + // @ts-expect-error TODO(@Peng): maybe fix this + stream = action(options); + if (!stream) return; + yield* stream; + }, }; } -export function actionToGenerateAnswer< - T extends keyof BlockSuitePresets.AIActions, ->( +function actionToGenerateAnswer( + host: EditorHost, id: T, variants?: Omit< Parameters[0], @@ -131,28 +128,29 @@ export function actionToGenerateAnswer< >, trackerOptions?: BlockSuitePresets.TrackerOptions ) { - return (host: EditorHost) => { - return ({ + return ({ + input, + signal, + update, + finish, + }: { + input: string; + signal?: AbortSignal; + update: (text: string) => void; + finish: (state: 'success' | 'error' | 'aborted', err?: AIError) => void; + }) => { + const { selectedBlocks: blocks } = getSelections(host); + if (!blocks || blocks.length === 0) return; + const stream = actionToStream( + host, + id, + input, signal, - update, - finish, - }: { - input: string; - signal?: AbortSignal; - update: (text: string) => void; - finish: (state: 'success' | 'error' | 'aborted', err?: AIError) => void; - }) => { - const { selectedBlocks: blocks } = getSelections(host); - if (!blocks || blocks.length === 0) return; - const stream = actionToStream( - id, - signal, - variants, - trackerOptions - )?.(host); - if (!stream) return; - bindTextStream(stream, { update, finish, signal }); - }; + variants, + trackerOptions + ); + if (!stream) return; + bindTextStream(stream, { update, finish, signal }); }; } @@ -174,10 +172,11 @@ function updateAIPanelConfig( const { config, host } = aiPanel; assertExists(config); config.generateAnswer = actionToGenerateAnswer( + host, id, variants, trackerOptions - )(host); + ); const ctx = new AIContext(); config.answerRenderer = actionToAnswerRenderer(id, host, ctx); @@ -206,7 +205,7 @@ export function actionToHandler( if (!blocks || blocks.length === 0) return; const block = blocks.at(-1); assertExists(block); - aiPanel.toggle(block, 'placeholder'); + aiPanel.toggle(block, ''); }; } @@ -222,43 +221,12 @@ export function handleInlineAskAIAction( if (!lastBlockPath) return; const block = host.view.getBlock(lastBlockPath); if (!block) return; - - const generateAnswer: AffineAIPanelWidgetConfig['generateAnswer'] = ({ - finish, - input, - signal, - update, - }) => { - if (!AIProvider.actions.chat) return; - - // recover selection to get content from above blocks - assertExists(selection); - host.selection.set([selection]); - - selectAboveBlocks(host) - .then(async context => { - if (!AIProvider.session || !AIProvider.actions.chat) return; - const sessionId = await AIProvider.session.createSession( - host.doc.workspace.id, - host.doc.id - ); - const stream = AIProvider.actions.chat({ - sessionId, - input: `${context}\n${input}`, - stream: true, - host, - where: 'inline-chat-panel', - control: 'chat-send', - docId: host.doc.id, - workspaceId: host.doc.workspace.id, - }); - bindTextStream(stream, { update, finish, signal }); - }) - .catch(console.error); - }; if (!panel.config) return; - panel.config.generateAnswer = generateAnswer; + updateAIPanelConfig(panel, 'chat', AIStarIconWithAnimation, undefined, { + control: 'chat-send', + where: 'inline-chat-panel', + }); if (!actionGroups) { panel.toggle(block); diff --git a/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-handler.ts b/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-handler.ts index e9e28c9b9c..241ea706c2 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-handler.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/actions/edgeless-handler.ts @@ -451,11 +451,7 @@ export function actionToHandler( togglePanel() .then(isEmpty => { - aiPanel.toggle( - referenceElement, - isEmpty ? undefined : 'placeholder', - false - ); + aiPanel.toggle(referenceElement, isEmpty ? undefined : '', false); }) .catch(console.error); }; diff --git a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts index aeea7df069..fef11066c9 100644 --- a/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/presets/ai/chat-panel/index.ts @@ -346,10 +346,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) { if (!this.doc) throw new Error('doc is required'); this._disposables.add( - AIProvider.slots.actions.on(({ action, event }) => { + AIProvider.slots.actions.on(({ event }) => { const { status } = this.chatContextValue; if ( - action !== 'chat' && event === 'finished' && (status === 'idle' || status === 'success') ) {