fix(core): slash ask ai returns make it real action result (#10349)

Fix issue [AF-2252](https://linear.app/affine-design/issue/AF-2252).

## What Changed?
- Remove useless `generateAnswer` configuration for inline ask ai
- Refactor the common `updateAIPanelConfig` function
- Use empty string instead of meaningless `placeholder` string
- Remove unnecessary high-order function wrappers
This commit is contained in:
akumatus
2025-02-21 12:52:53 +00:00
parent 55651503df
commit 07cbf5affe
4 changed files with 75 additions and 112 deletions
@@ -278,7 +278,7 @@ export class AffineAIPanelWidget extends WidgetComponent {
input?: string, input?: string,
shouldTriggerCallback?: boolean shouldTriggerCallback?: boolean
) => { ) => {
if (input) { if (typeof input === 'string') {
this._inputText = input; this._inputText = input;
this.generate(); this.generate();
} else { } else {
@@ -1,9 +1,9 @@
import { type EditorHost, TextSelection } from '@blocksuite/affine/block-std'; import { type EditorHost, TextSelection } from '@blocksuite/affine/block-std';
import { import {
type AffineAIPanelWidget, type AffineAIPanelWidget,
type AffineAIPanelWidgetConfig,
type AIError, type AIError,
type AIItemGroupConfig, type AIItemGroupConfig,
AIStarIconWithAnimation,
createLitPortal, createLitPortal,
} from '@blocksuite/affine/blocks'; } from '@blocksuite/affine/blocks';
import { assertExists } from '@blocksuite/affine/global/utils'; import { assertExists } from '@blocksuite/affine/global/utils';
@@ -64,66 +64,63 @@ export function bindTextStream(
}); });
} }
export function actionToStream<T extends keyof BlockSuitePresets.AIActions>( function actionToStream<T extends keyof BlockSuitePresets.AIActions>(
host: EditorHost,
id: T, id: T,
input: string,
signal?: AbortSignal, signal?: AbortSignal,
variants?: Omit< variants?: Omit<
Parameters<BlockSuitePresets.AIActions[T]>[0], Parameters<BlockSuitePresets.AIActions[T]>[0],
keyof BlockSuitePresets.AITextActionOptions keyof BlockSuitePresets.AITextActionOptions
>, >,
trackerOptions?: BlockSuitePresets.TrackerOptions trackerOptions?: BlockSuitePresets.TrackerOptions
) { ): BlockSuitePresets.TextStream | undefined {
const action = AIProvider.actions[id]; const action = AIProvider.actions[id];
if (!action || typeof action !== 'function') return; 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 stream: BlockSuitePresets.TextStream | undefined;
let attachments: File[] = []; return {
async *[Symbol.asyncIterator]() {
const { currentTextSelection, selectedBlocks } = getSelections(host);
if (currentTextSelection?.isCollapsed()) { let markdown: string;
markdown = await selectAboveBlocks(host); let attachments: File[] = [];
} else {
[markdown, attachments] = await Promise.all([
getSelectedTextContent(host),
getSelectedImagesAsBlobs(host),
]);
}
// for now if there are more than one selected blocks, we will not omit the attachments if (currentTextSelection?.isCollapsed()) {
const sendAttachments = markdown = await selectAboveBlocks(host);
selectedBlocks?.length === 1 && attachments.length > 0; } else {
const models = selectedBlocks?.map(block => block.model); [markdown, attachments] = await Promise.all([
const control = trackerOptions?.control ?? 'format-bar'; getSelectedTextContent(host),
const where = trackerOptions?.where ?? 'ai-panel'; getSelectedImagesAsBlobs(host),
const options = { ]);
...variants, }
attachments: sendAttachments ? attachments : undefined,
input: sendAttachments ? '' : markdown, const models = selectedBlocks?.map(block => block.model);
stream: true, const control = trackerOptions?.control ?? 'format-bar';
host, const where = trackerOptions?.where ?? 'ai-panel';
models, const options = {
signal, ...variants,
control, attachments,
where, input: input ? `${markdown}\n${input}` : markdown,
docId: host.doc.id, stream: true,
workspaceId: host.doc.workspace.id, host,
} as Parameters<typeof action>[0]; models,
// @ts-expect-error TODO(@Peng): maybe fix this signal,
stream = action(options); control,
if (!stream) return; where,
yield* stream; docId: host.doc.id,
}, workspaceId: host.doc.workspace.id,
}; } as Parameters<typeof action>[0];
// @ts-expect-error TODO(@Peng): maybe fix this
stream = action(options);
if (!stream) return;
yield* stream;
},
}; };
} }
export function actionToGenerateAnswer< function actionToGenerateAnswer<T extends keyof BlockSuitePresets.AIActions>(
T extends keyof BlockSuitePresets.AIActions, host: EditorHost,
>(
id: T, id: T,
variants?: Omit< variants?: Omit<
Parameters<BlockSuitePresets.AIActions[T]>[0], Parameters<BlockSuitePresets.AIActions[T]>[0],
@@ -131,28 +128,29 @@ export function actionToGenerateAnswer<
>, >,
trackerOptions?: BlockSuitePresets.TrackerOptions 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, signal,
update, variants,
finish, trackerOptions
}: { );
input: string; if (!stream) return;
signal?: AbortSignal; bindTextStream(stream, { update, finish, signal });
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 });
};
}; };
} }
@@ -174,10 +172,11 @@ function updateAIPanelConfig<T extends keyof BlockSuitePresets.AIActions>(
const { config, host } = aiPanel; const { config, host } = aiPanel;
assertExists(config); assertExists(config);
config.generateAnswer = actionToGenerateAnswer( config.generateAnswer = actionToGenerateAnswer(
host,
id, id,
variants, variants,
trackerOptions trackerOptions
)(host); );
const ctx = new AIContext(); const ctx = new AIContext();
config.answerRenderer = actionToAnswerRenderer(id, host, ctx); config.answerRenderer = actionToAnswerRenderer(id, host, ctx);
@@ -206,7 +205,7 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
if (!blocks || blocks.length === 0) return; if (!blocks || blocks.length === 0) return;
const block = blocks.at(-1); const block = blocks.at(-1);
assertExists(block); assertExists(block);
aiPanel.toggle(block, 'placeholder'); aiPanel.toggle(block, '');
}; };
} }
@@ -222,43 +221,12 @@ export function handleInlineAskAIAction(
if (!lastBlockPath) return; if (!lastBlockPath) return;
const block = host.view.getBlock(lastBlockPath); const block = host.view.getBlock(lastBlockPath);
if (!block) return; 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; if (!panel.config) return;
panel.config.generateAnswer = generateAnswer; updateAIPanelConfig(panel, 'chat', AIStarIconWithAnimation, undefined, {
control: 'chat-send',
where: 'inline-chat-panel',
});
if (!actionGroups) { if (!actionGroups) {
panel.toggle(block); panel.toggle(block);
@@ -451,11 +451,7 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
togglePanel() togglePanel()
.then(isEmpty => { .then(isEmpty => {
aiPanel.toggle( aiPanel.toggle(referenceElement, isEmpty ? undefined : '', false);
referenceElement,
isEmpty ? undefined : 'placeholder',
false
);
}) })
.catch(console.error); .catch(console.error);
}; };
@@ -346,10 +346,9 @@ export class ChatPanel extends WithDisposable(ShadowlessElement) {
if (!this.doc) throw new Error('doc is required'); if (!this.doc) throw new Error('doc is required');
this._disposables.add( this._disposables.add(
AIProvider.slots.actions.on(({ action, event }) => { AIProvider.slots.actions.on(({ event }) => {
const { status } = this.chatContextValue; const { status } = this.chatContextValue;
if ( if (
action !== 'chat' &&
event === 'finished' && event === 'finished' &&
(status === 'idle' || status === 'success') (status === 'idle' || status === 'success')
) { ) {