From 9f94d5c216222e353a6074dde4a21ae0467d0584 Mon Sep 17 00:00:00 2001 From: Wu Yue Date: Sat, 27 Sep 2025 19:58:58 +0800 Subject: [PATCH] feat(core): support ai chat delete action (#13655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 截屏2025-09-26 10 58 39 ## Summary by CodeRabbit - New Features - Delete icon added to AI session history with tooltip and confirmation prompt; deleting current session opens a new session. - Session deletion wired end-to-end (toolbar → provider → backend) and shows notifications. - Improvements - Cleanup now supports deleting sessions with or without a document ID (document-specific or workspace-wide). - UI tweaks for cleaner session item layout and safer click handling (delete won’t trigger item click). --- .../server/src/plugins/copilot/resolver.ts | 23 +++++++-- packages/backend/server/src/schema.gql | 2 +- packages/common/graphql/src/schema.ts | 2 +- .../core/src/blocksuite/ai/actions/types.ts | 2 +- .../src/blocksuite/ai/chat-panel/ai-title.ts | 4 ++ .../src/blocksuite/ai/chat-panel/index.ts | 26 ++++++++++ .../ai-chat-toolbar/ai-chat-toolbar.ts | 7 +++ .../ai-chat-toolbar/ai-session-history.ts | 50 ++++++++++++++++++- .../blocksuite/ai/provider/copilot-client.ts | 2 +- .../blocksuite/ai/provider/setup-provider.tsx | 2 +- 10 files changed, 108 insertions(+), 12 deletions(-) diff --git a/packages/backend/server/src/plugins/copilot/resolver.ts b/packages/backend/server/src/plugins/copilot/resolver.ts index 85f53e6160..68e829d843 100644 --- a/packages/backend/server/src/plugins/copilot/resolver.ts +++ b/packages/backend/server/src/plugins/copilot/resolver.ts @@ -125,8 +125,8 @@ class DeleteSessionInput { @Field(() => String) workspaceId!: string; - @Field(() => String) - docId!: string; + @Field(() => String, { nullable: true }) + docId!: string | undefined; @Field(() => [String]) sessionIds!: string[]; @@ -737,11 +737,24 @@ export class CopilotResolver { @Args({ name: 'options', type: () => DeleteSessionInput }) options: DeleteSessionInput ): Promise { - await this.ac.user(user.id).doc(options).allowLocal().assert('Doc.Update'); - if (!options.sessionIds.length) { + const { workspaceId, docId, sessionIds } = options; + if (docId) { + await this.ac + .user(user.id) + .doc({ workspaceId, docId }) + .allowLocal() + .assert('Doc.Update'); + } else { + await this.ac + .user(user.id) + .workspace(workspaceId) + .allowLocal() + .assert('Workspace.Copilot'); + } + if (!sessionIds.length) { throw new NotFoundException('Session not found'); } - const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${options.workspaceId}`; + const lockFlag = `${COPILOT_LOCKER}:session:${user.id}:${workspaceId}`; await using lock = await this.mutex.acquire(lockFlag); if (!lock) { throw new TooManyRequest('Server is busy'); diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 3f2d10b010..1c8d8519e4 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -542,7 +542,7 @@ type DeleteAccount { } input DeleteSessionInput { - docId: String! + docId: String sessionIds: [String!]! workspaceId: String! } diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index 07adda5dd1..5f07a20489 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -654,7 +654,7 @@ export interface DeleteAccount { } export interface DeleteSessionInput { - docId: Scalars['String']['input']; + docId?: InputMaybe; sessionIds: Array; workspaceId: Scalars['String']['input']; } diff --git a/packages/frontend/core/src/blocksuite/ai/actions/types.ts b/packages/frontend/core/src/blocksuite/ai/actions/types.ts index c915e2e2d8..3287834618 100644 --- a/packages/frontend/core/src/blocksuite/ai/actions/types.ts +++ b/packages/frontend/core/src/blocksuite/ai/actions/types.ts @@ -441,7 +441,7 @@ declare global { ) => Promise; cleanup: ( workspaceId: string, - docId: string, + docId: string | undefined, sessionIds: string[] ) => Promise; ids: ( diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts index 0b9c6c289f..cc9e238223 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/ai-title.ts @@ -130,6 +130,9 @@ export class AIChatPanelTitle extends SignalWatcher( @property({ attribute: false }) accessor openDoc!: (docId: string, sessionId: string) => void; + @property({ attribute: false }) + accessor deleteSession!: (session: BlockSuitePresets.AIRecentSession) => void; + private readonly openPlayground = () => { const playgroundContent = html` diff --git a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts index 50ffdbbe5c..63f42e73b0 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -237,6 +237,31 @@ export class ChatPanel extends SignalWatcher( return this.session; }; + private readonly deleteSession = async ( + session: BlockSuitePresets.AIRecentSession + ) => { + if (!AIProvider.histories) { + return; + } + const confirm = await this.notificationService.confirm({ + title: 'Delete this history?', + message: + 'Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.', + confirmText: 'Delete', + cancelText: 'Cancel', + }); + if (confirm) { + await AIProvider.histories.cleanup( + session.workspaceId, + session.docId || undefined, + [session.sessionId] + ); + if (session.sessionId === this.session?.sessionId) { + this.newSession(); + } + } + }; + private readonly updateSession = async (options: UpdateChatSessionInput) => { await AIProvider.session?.updateSession(options); const session = await AIProvider.session?.getSession( @@ -413,6 +438,7 @@ export class ChatPanel extends SignalWatcher( .togglePin=${this.togglePin} .openSession=${this.openSession} .openDoc=${this.openDoc} + .deleteSession=${this.deleteSession} > ${keyed( this.hasPinned ? this.session?.sessionId : this.doc.id, diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts index 26e21265ea..9ba8fcf55c 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-chat-toolbar.ts @@ -42,6 +42,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onOpenDoc!: (docId: string, sessionId: string) => void; + @property({ attribute: false }) + accessor onSessionDelete!: ( + session: BlockSuitePresets.AIRecentSession + ) => void; + @property({ attribute: false }) accessor docDisplayConfig!: DocDisplayConfig; @@ -198,7 +203,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { .workspaceId=${this.workspaceId} .docDisplayConfig=${this.docDisplayConfig} .onSessionClick=${this.onSessionClick} + .onSessionDelete=${this.onSessionDelete} .onDocClick=${this.onDocClick} + .notificationService=${this.notificationService} > `, portalStyles: { diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts index a6f3ecf0d7..acb953a591 100644 --- a/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts +++ b/packages/frontend/core/src/blocksuite/ai/components/ai-chat-toolbar/ai-session-history.ts @@ -3,6 +3,7 @@ import { WithDisposable } from '@blocksuite/affine/global/lit'; import { scrollbarStyle } from '@blocksuite/affine/shared/styles'; import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme'; import { ShadowlessElement } from '@blocksuite/affine/std'; +import { DeleteIcon } from '@blocksuite/icons/lit'; import { css, html, nothing, type PropertyValues } from 'lit'; import { property, query, state } from 'lit/decorators.js'; @@ -62,7 +63,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { position: relative; display: flex; height: 24px; - padding: 2px 4px; justify-content: space-between; align-items: center; border-radius: 4px; @@ -85,6 +85,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { font-size: 12px; font-weight: 400; line-height: 20px; + padding: 2px 4px; color: ${unsafeCSSVarV2('text/primary')}; overflow: hidden; text-overflow: ellipsis; @@ -94,7 +95,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { .ai-session-doc { display: flex; width: 120px; - padding: 0px 4px; + padding: 2px; align-items: center; gap: 4px; flex-shrink: 0; @@ -117,6 +118,36 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { white-space: nowrap; } } + + .ai-session-item-delete { + position: absolute; + right: 2px; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + background: ${unsafeCSSVarV2('layer/background/primary')}; + border-radius: 2px; + padding: 2px; + cursor: pointer; + opacity: 0; + visibility: hidden; + transition: + opacity 0.2s ease, + visibility 0.2s ease; + + svg { + width: 16px; + height: 16px; + color: ${unsafeCSSVarV2('icon/primary')}; + } + } + + .ai-session-item:hover .ai-session-item-delete { + opacity: 1; + visibility: visible; + } } ${scrollbarStyle('.ai-session-history')} @@ -134,6 +165,11 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor onSessionClick!: (sessionId: string) => void; + @property({ attribute: false }) + accessor onSessionDelete!: ( + session: BlockSuitePresets.AIRecentSession + ) => void; + @property({ attribute: false }) accessor onDocClick!: (docId: string, sessionId: string) => void; @@ -272,6 +308,16 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) { ${session.docId ? this.renderSessionDoc(session.docId, session.sessionId) : nothing} +
{ + e.stopPropagation(); + this.onSessionDelete(session); + }} + > + ${DeleteIcon()} + Delete +
`; })} diff --git a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts index 8e8b7028c2..1fdff183a0 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts +++ b/packages/frontend/core/src/blocksuite/ai/provider/copilot-client.ts @@ -261,7 +261,7 @@ export class CopilotClient { async cleanupSessions(input: { workspaceId: string; - docId: string; + docId: string | undefined; sessionIds: string[]; }) { try { diff --git a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx index ceb97f8f37..91248e0d4c 100644 --- a/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx +++ b/packages/frontend/core/src/blocksuite/ai/provider/setup-provider.tsx @@ -794,7 +794,7 @@ Could you make a new website based on these notes and send back just the html fi }, cleanup: async ( workspaceId: string, - docId: string, + docId: string | undefined, sessionIds: string[] ) => { await client.cleanupSessions({ workspaceId, docId, sessionIds });