feat(core): show stop modal if clickoutside during generating (#12227)

### TL;DR

feat: show stop model if click-outside during ai generating

>CLOSE AI-89

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added a confirmation dialog when attempting to stop AI content generation by clicking outside the panel, ensuring users can confirm or cancel the stop action.

- **Tests**
  - Introduced an end-to-end test to verify the confirmation dialog appears and AI generation stops as expected when clicking outside during generation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
yoyoyohamapi
2025-05-14 03:16:55 +00:00
parent 291bb9c449
commit f737327f12
2 changed files with 48 additions and 1 deletions

View File

@@ -113,7 +113,11 @@ export class AffineAIPanelWidget extends WidgetComponent {
}; };
private readonly _clickOutside = () => { private readonly _clickOutside = () => {
this._discardWithConfirmation(); if (this.state === 'generating') {
this._stopWithConfirmation();
} else {
this._discardWithConfirmation();
}
}; };
private _discardModalAbort: AbortController | null = null; private _discardModalAbort: AbortController | null = null;
@@ -169,6 +173,16 @@ export class AffineAIPanelWidget extends WidgetComponent {
ctx: unknown = null; ctx: unknown = null;
private readonly _stopWithConfirmation = () => {
this.showStopModal()
.then(stop => {
if (stop) {
this.stopGenerating();
}
})
.catch(console.error);
};
private readonly _discardWithConfirmation = () => { private readonly _discardWithConfirmation = () => {
if (this.state === 'hidden') { if (this.state === 'hidden') {
return; return;
@@ -275,6 +289,24 @@ export class AffineAIPanelWidget extends WidgetComponent {
this._autoUpdatePosition(reference); this._autoUpdatePosition(reference);
}; };
showStopModal = () => {
const notification = this.host.std.getOptional(NotificationProvider);
if (!notification) {
return Promise.resolve(true);
}
this._clearDiscardModal();
this._discardModalAbort = new AbortController();
return notification
.confirm({
title: 'Stop generating',
message: 'AI is generating content. Do you want to stop generating?',
cancelText: 'Cancel',
confirmText: 'Stop',
abort: this._abortController.signal,
})
.finally(() => (this._discardModalAbort = null));
};
showDiscardModal = () => { showDiscardModal = () => {
const notification = this.host.std.getOptional(NotificationProvider); const notification = this.host.std.getOptional(NotificationProvider);
if (!notification) { if (!notification) {

View File

@@ -21,6 +21,21 @@ test.describe('AIChatWith/Text', () => {
await expect(page.getByTestId('ai-generating')).not.toBeVisible(); await expect(page.getByTestId('ai-generating')).not.toBeVisible();
}); });
test('should support stop generating when click outside', async ({
loggedInPage: page,
utils,
}) => {
await utils.editor.askAIWithText(page, 'Panda');
await page.getByTestId('action-generate-image').click();
await expect(page.getByTestId('ai-generating')).toBeVisible();
await page.mouse.click(0, 0);
await expect(
page.getByText('AI is generating content. Do you want to stop generating')
).toBeVisible();
await page.getByTestId('confirm-modal-confirm').click();
await expect(page.getByTestId('ai-generating')).not.toBeVisible();
});
test('should support copy answer', async ({ loggedInPage: page, utils }) => { test('should support copy answer', async ({ loggedInPage: page, utils }) => {
const { translate } = await utils.editor.askAIWithText(page, 'Apple'); const { translate } = await utils.editor.askAIWithText(page, 'Apple');
const { answer } = await translate('German'); const { answer } = await translate('German');