feat(core): support ai chat delete action (#13655)
<img width="411" height="205" alt="截屏2025-09-26 10 58 39" src="https://github.com/user-attachments/assets/c3bce144-7847-4794-b766-5a3777cbc00d" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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). <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -125,8 +125,8 @@ class DeleteSessionInput {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
workspaceId!: string;
|
workspaceId!: string;
|
||||||
|
|
||||||
@Field(() => String)
|
@Field(() => String, { nullable: true })
|
||||||
docId!: string;
|
docId!: string | undefined;
|
||||||
|
|
||||||
@Field(() => [String])
|
@Field(() => [String])
|
||||||
sessionIds!: string[];
|
sessionIds!: string[];
|
||||||
@@ -737,11 +737,24 @@ export class CopilotResolver {
|
|||||||
@Args({ name: 'options', type: () => DeleteSessionInput })
|
@Args({ name: 'options', type: () => DeleteSessionInput })
|
||||||
options: DeleteSessionInput
|
options: DeleteSessionInput
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
await this.ac.user(user.id).doc(options).allowLocal().assert('Doc.Update');
|
const { workspaceId, docId, sessionIds } = options;
|
||||||
if (!options.sessionIds.length) {
|
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');
|
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);
|
await using lock = await this.mutex.acquire(lockFlag);
|
||||||
if (!lock) {
|
if (!lock) {
|
||||||
throw new TooManyRequest('Server is busy');
|
throw new TooManyRequest('Server is busy');
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ type DeleteAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input DeleteSessionInput {
|
input DeleteSessionInput {
|
||||||
docId: String!
|
docId: String
|
||||||
sessionIds: [String!]!
|
sessionIds: [String!]!
|
||||||
workspaceId: String!
|
workspaceId: String!
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -654,7 +654,7 @@ export interface DeleteAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteSessionInput {
|
export interface DeleteSessionInput {
|
||||||
docId: Scalars['String']['input'];
|
docId?: InputMaybe<Scalars['String']['input']>;
|
||||||
sessionIds: Array<Scalars['String']['input']>;
|
sessionIds: Array<Scalars['String']['input']>;
|
||||||
workspaceId: Scalars['String']['input'];
|
workspaceId: Scalars['String']['input'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ declare global {
|
|||||||
) => Promise<AIHistory[] | undefined>;
|
) => Promise<AIHistory[] | undefined>;
|
||||||
cleanup: (
|
cleanup: (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string,
|
docId: string | undefined,
|
||||||
sessionIds: string[]
|
sessionIds: string[]
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
ids: (
|
ids: (
|
||||||
|
|||||||
@@ -130,6 +130,9 @@ export class AIChatPanelTitle extends SignalWatcher(
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor openDoc!: (docId: string, sessionId: string) => void;
|
accessor openDoc!: (docId: string, sessionId: string) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor deleteSession!: (session: BlockSuitePresets.AIRecentSession) => void;
|
||||||
|
|
||||||
private readonly openPlayground = () => {
|
private readonly openPlayground = () => {
|
||||||
const playgroundContent = html`
|
const playgroundContent = html`
|
||||||
<playground-content
|
<playground-content
|
||||||
@@ -182,6 +185,7 @@ export class AIChatPanelTitle extends SignalWatcher(
|
|||||||
.onTogglePin=${this.togglePin}
|
.onTogglePin=${this.togglePin}
|
||||||
.onOpenSession=${this.openSession}
|
.onOpenSession=${this.openSession}
|
||||||
.onOpenDoc=${this.openDoc}
|
.onOpenDoc=${this.openDoc}
|
||||||
|
.onSessionDelete=${this.deleteSession}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.notificationService=${this.notificationService}
|
.notificationService=${this.notificationService}
|
||||||
></ai-chat-toolbar>
|
></ai-chat-toolbar>
|
||||||
|
|||||||
@@ -237,6 +237,31 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
return this.session;
|
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) => {
|
private readonly updateSession = async (options: UpdateChatSessionInput) => {
|
||||||
await AIProvider.session?.updateSession(options);
|
await AIProvider.session?.updateSession(options);
|
||||||
const session = await AIProvider.session?.getSession(
|
const session = await AIProvider.session?.getSession(
|
||||||
@@ -413,6 +438,7 @@ export class ChatPanel extends SignalWatcher(
|
|||||||
.togglePin=${this.togglePin}
|
.togglePin=${this.togglePin}
|
||||||
.openSession=${this.openSession}
|
.openSession=${this.openSession}
|
||||||
.openDoc=${this.openDoc}
|
.openDoc=${this.openDoc}
|
||||||
|
.deleteSession=${this.deleteSession}
|
||||||
></ai-chat-panel-title>
|
></ai-chat-panel-title>
|
||||||
${keyed(
|
${keyed(
|
||||||
this.hasPinned ? this.session?.sessionId : this.doc.id,
|
this.hasPinned ? this.session?.sessionId : this.doc.id,
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onOpenDoc!: (docId: string, sessionId: string) => void;
|
accessor onOpenDoc!: (docId: string, sessionId: string) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onSessionDelete!: (
|
||||||
|
session: BlockSuitePresets.AIRecentSession
|
||||||
|
) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor docDisplayConfig!: DocDisplayConfig;
|
accessor docDisplayConfig!: DocDisplayConfig;
|
||||||
|
|
||||||
@@ -198,7 +203,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
|
|||||||
.workspaceId=${this.workspaceId}
|
.workspaceId=${this.workspaceId}
|
||||||
.docDisplayConfig=${this.docDisplayConfig}
|
.docDisplayConfig=${this.docDisplayConfig}
|
||||||
.onSessionClick=${this.onSessionClick}
|
.onSessionClick=${this.onSessionClick}
|
||||||
|
.onSessionDelete=${this.onSessionDelete}
|
||||||
.onDocClick=${this.onDocClick}
|
.onDocClick=${this.onDocClick}
|
||||||
|
.notificationService=${this.notificationService}
|
||||||
></ai-session-history>
|
></ai-session-history>
|
||||||
`,
|
`,
|
||||||
portalStyles: {
|
portalStyles: {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { WithDisposable } from '@blocksuite/affine/global/lit';
|
|||||||
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
import { scrollbarStyle } from '@blocksuite/affine/shared/styles';
|
||||||
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
|
||||||
import { ShadowlessElement } from '@blocksuite/affine/std';
|
import { ShadowlessElement } from '@blocksuite/affine/std';
|
||||||
|
import { DeleteIcon } from '@blocksuite/icons/lit';
|
||||||
import { css, html, nothing, type PropertyValues } from 'lit';
|
import { css, html, nothing, type PropertyValues } from 'lit';
|
||||||
import { property, query, state } from 'lit/decorators.js';
|
import { property, query, state } from 'lit/decorators.js';
|
||||||
|
|
||||||
@@ -62,7 +63,6 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
padding: 2px 4px;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@@ -85,6 +85,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
|
padding: 2px 4px;
|
||||||
color: ${unsafeCSSVarV2('text/primary')};
|
color: ${unsafeCSSVarV2('text/primary')};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -94,7 +95,7 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
.ai-session-doc {
|
.ai-session-doc {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
padding: 0px 4px;
|
padding: 2px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -117,6 +118,36 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
white-space: nowrap;
|
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')}
|
${scrollbarStyle('.ai-session-history')}
|
||||||
@@ -134,6 +165,11 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onSessionClick!: (sessionId: string) => void;
|
accessor onSessionClick!: (sessionId: string) => void;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
accessor onSessionDelete!: (
|
||||||
|
session: BlockSuitePresets.AIRecentSession
|
||||||
|
) => void;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
accessor onDocClick!: (docId: string, sessionId: string) => void;
|
accessor onDocClick!: (docId: string, sessionId: string) => void;
|
||||||
|
|
||||||
@@ -272,6 +308,16 @@ export class AISessionHistory extends WithDisposable(ShadowlessElement) {
|
|||||||
${session.docId
|
${session.docId
|
||||||
? this.renderSessionDoc(session.docId, session.sessionId)
|
? this.renderSessionDoc(session.docId, session.sessionId)
|
||||||
: nothing}
|
: nothing}
|
||||||
|
<div
|
||||||
|
class="ai-session-item-delete"
|
||||||
|
@click=${(e: MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.onSessionDelete(session);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
${DeleteIcon()}
|
||||||
|
<affine-tooltip>Delete</affine-tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ export class CopilotClient {
|
|||||||
|
|
||||||
async cleanupSessions(input: {
|
async cleanupSessions(input: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
docId: string;
|
docId: string | undefined;
|
||||||
sessionIds: string[];
|
sessionIds: string[];
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -794,7 +794,7 @@ Could you make a new website based on these notes and send back just the html fi
|
|||||||
},
|
},
|
||||||
cleanup: async (
|
cleanup: async (
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
docId: string,
|
docId: string | undefined,
|
||||||
sessionIds: string[]
|
sessionIds: string[]
|
||||||
) => {
|
) => {
|
||||||
await client.cleanupSessions({ workspaceId, docId, sessionIds });
|
await client.cleanupSessions({ workspaceId, docId, sessionIds });
|
||||||
|
|||||||
Reference in New Issue
Block a user