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

View File

@@ -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<T extends keyof BlockSuitePresets.AIActions>(
function actionToStream<T extends keyof BlockSuitePresets.AIActions>(
host: EditorHost,
id: T,
input: string,
signal?: AbortSignal,
variants?: Omit<
Parameters<BlockSuitePresets.AIActions[T]>[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<typeof action>[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<typeof action>[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<T extends keyof BlockSuitePresets.AIActions>(
host: EditorHost,
id: T,
variants?: Omit<
Parameters<BlockSuitePresets.AIActions[T]>[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<T extends keyof BlockSuitePresets.AIActions>(
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<T extends keyof BlockSuitePresets.AIActions>(
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);

View File

@@ -451,11 +451,7 @@ export function actionToHandler<T extends keyof BlockSuitePresets.AIActions>(
togglePanel()
.then(isEmpty => {
aiPanel.toggle(
referenceElement,
isEmpty ? undefined : 'placeholder',
false
);
aiPanel.toggle(referenceElement, isEmpty ? undefined : '', false);
})
.catch(console.error);
};

View File

@@ -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')
) {