fix: chat session cannot delete (#14312)

fix #14309



#### PR Dependency Tree


* **PR #14312** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

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

## Summary by CodeRabbit

* **New Features**
* Added AI chat session deletion with confirmation dialogs and
success/failure notifications.
* Localized AI chat panel labels, loading messages, and session
management text across multiple languages.

* **Documentation**
* Added internationalization support for chat panel titles, history
loading states, and deletion confirmations.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-01-26 19:47:47 +08:00
committed by GitHub
parent 5498133627
commit 27ed15a83e
11 changed files with 306 additions and 104 deletions

View File

@@ -0,0 +1,44 @@
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import type { NotificationService } from '@blocksuite/affine/shared/services';
import type { DocDisplayConfig } from '../ai-chat-chips';
import type { ChatStatus } from '../ai-chat-messages';
import { AIChatToolbar } from './ai-chat-toolbar';
export type ConfigureAIChatToolbarOptions = {
session: CopilotChatHistoryFragment | null | undefined;
workspaceId: string;
docId?: string;
status: ChatStatus;
docDisplayConfig: DocDisplayConfig;
notificationService: NotificationService;
onNewSession: () => void;
onTogglePin: () => Promise<void>;
onOpenSession: (sessionId: string) => void;
onOpenDoc: (docId: string, sessionId: string) => void;
onSessionDelete: (session: BlockSuitePresets.AIRecentSession) => void;
};
export function getOrCreateAIChatToolbar(
current: AIChatToolbar | null | undefined
): AIChatToolbar {
return current ?? new AIChatToolbar();
}
export function configureAIChatToolbar(
tool: AIChatToolbar,
options: ConfigureAIChatToolbarOptions
): AIChatToolbar {
tool.session = options.session;
tool.workspaceId = options.workspaceId;
tool.docId = options.docId;
tool.status = options.status;
tool.docDisplayConfig = options.docDisplayConfig;
tool.notificationService = options.notificationService;
tool.onNewSession = options.onNewSession;
tool.onTogglePin = options.onTogglePin;
tool.onOpenSession = options.onOpenSession;
tool.onOpenDoc = options.onOpenDoc;
tool.onSessionDelete = options.onSessionDelete;
return tool;
}

View File

@@ -1,2 +1,3 @@
export * from './ai-chat-toolbar';
export * from './ai-session-history';
export * from './configure-ai-chat-toolbar';

View File

@@ -89,6 +89,7 @@ const AllDocsButton = () => {
};
const AIChatButton = () => {
const t = useI18n();
const featureFlagService = useService(FeatureFlagService);
const serverService = useService(ServerService);
const serverFeatures = useLiveData(serverService.server.features$);
@@ -108,7 +109,9 @@ const AIChatButton = () => {
return (
<MenuLinkItem icon={<AiOutlineIcon />} active={aiChatActive} to={'/chat'}>
<span data-testid="ai-chat">Intelligence</span>
<span data-testid="ai-chat">
{t['com.affine.workspaceSubPath.chat']()}
</span>
</MenuLinkItem>
);
};

View File

@@ -0,0 +1,61 @@
import type { I18nInstance } from '@affine/i18n';
import type { NotificationService } from '@blocksuite/affine/shared/services';
export type SessionDeleteCleanupFn = (
session: BlockSuitePresets.AIRecentSession
) => Promise<void>;
export type CreateSessionDeleteHandlerOptions = {
t: I18nInstance;
notificationService: NotificationService;
cleanupSession: SessionDeleteCleanupFn;
canDeleteSession?: (session: BlockSuitePresets.AIRecentSession) => boolean;
isActiveSession?: (session: BlockSuitePresets.AIRecentSession) => boolean;
onActiveSessionDeleted?: () => void;
};
export function createSessionDeleteHandler({
t,
notificationService,
cleanupSession,
canDeleteSession,
isActiveSession,
onActiveSessionDeleted,
}: CreateSessionDeleteHandlerOptions) {
return async (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
if (canDeleteSession && !canDeleteSession(sessionToDelete)) {
notificationService.toast(
t['com.affine.ai.chat-panel.session.delete.toast.failed']()
);
return;
}
const confirm = await notificationService.confirm({
title: t['com.affine.ai.chat-panel.session.delete.confirm.title'](),
message: t['com.affine.ai.chat-panel.session.delete.confirm.message'](),
confirmText: t['Delete'](),
cancelText: t['Cancel'](),
});
if (!confirm) {
return;
}
try {
await cleanupSession(sessionToDelete);
notificationService.toast(
t['com.affine.ai.chat-panel.session.delete.toast.success']()
);
} catch (error) {
console.error(error);
notificationService.toast(
t['com.affine.ai.chat-panel.session.delete.toast.failed']()
);
return;
}
if (isActiveSession?.(sessionToDelete)) {
onActiveSessionDeleted?.();
}
};
}

View File

@@ -5,7 +5,11 @@ import {
type ChatContextValue,
} from '@affine/core/blocksuite/ai/components/ai-chat-content';
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
import {
AIChatToolbar,
configureAIChatToolbar,
getOrCreateAIChatToolbar,
} from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
import { getViewManager } from '@affine/core/blocksuite/manager/view';
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
@@ -37,6 +41,7 @@ import {
WorkbenchService,
} from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { useI18n } from '@affine/i18n';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import { BlockStdScope } from '@blocksuite/affine/std';
import type { Workspace } from '@blocksuite/affine/store';
@@ -45,6 +50,7 @@ import { useFramework, useService } from '@toeverything/infra';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createSessionDeleteHandler } from '../chat-panel-utils';
import * as styles from './index.css';
type CopilotSession = Awaited<ReturnType<CopilotClient['getSession']>>;
@@ -88,6 +94,7 @@ function useMockStd() {
}
export const Component = () => {
const t = useI18n();
const framework = useFramework();
const [isBodyProvided, setIsBodyProvided] = useState(false);
const [isHeaderProvided, setIsHeaderProvided] = useState(false);
@@ -193,10 +200,46 @@ export const Component = () => {
);
const confirmModal = useConfirmModal();
const notificationService = useMemo(
() =>
new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
),
[confirmModal.closeConfirmModal, confirmModal.openConfirmModal]
);
const specs = useAISpecs();
const mockStd = useMockStd();
const handleAISubscribe = useAISubscribe();
const deleteSession = useMemo(
() =>
createSessionDeleteHandler({
t,
notificationService,
cleanupSession: async sessionToDelete => {
await client.cleanupSessions({
workspaceId: sessionToDelete.workspaceId,
docId: sessionToDelete.docId || undefined,
sessionIds: [sessionToDelete.sessionId],
});
},
isActiveSession: sessionToDelete =>
sessionToDelete.sessionId === currentSession?.sessionId,
onActiveSessionDeleted: () => {
setCurrentSession(null);
reMountChatContent();
},
}),
[
client,
currentSession?.sessionId,
notificationService,
reMountChatContent,
t,
]
);
// init or update ai-chat-content
useEffect(() => {
if (!isBodyProvided) {
@@ -223,10 +266,7 @@ export const Component = () => {
);
content.peekViewService = framework.get(PeekViewService);
content.affineThemeService = framework.get(AppThemeService);
content.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
);
content.notificationService = notificationService;
content.aiDraftService = framework.get(AIDraftService);
content.aiToolsConfigService = framework.get(AIToolsConfigService);
content.serverService = framework.get(ServerService);
@@ -258,6 +298,7 @@ export const Component = () => {
workspaceId,
confirmModal,
onContextChange,
notificationService,
specs,
onOpenDoc,
handleAISubscribe,
@@ -268,39 +309,31 @@ export const Component = () => {
if (!isHeaderProvided || !chatToolContainerRef.current) {
return;
}
let tool = chatTool;
if (!tool) {
tool = new AIChatToolbar();
}
tool.session = currentSession;
tool.workspaceId = workspaceId;
tool.status = status;
tool.docDisplayConfig = docDisplayConfig;
tool.onOpenSession = onOpenSession;
tool.notificationService = new NotificationServiceImpl(
confirmModal.closeConfirmModal,
confirmModal.openConfirmModal
);
tool.onNewSession = () => {
if (!currentSession) return;
setCurrentSession(null);
reMountChatContent();
};
tool.onTogglePin = async () => {
await togglePin();
};
tool.onOpenDoc = (docId: string, sessionId: string) => {
const { workbench } = framework.get(WorkbenchService);
const viewService = framework.get(ViewService);
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
workbench.openSidebar();
viewService.view.activeSidebarTab('chat');
};
const tool = getOrCreateAIChatToolbar(chatTool);
configureAIChatToolbar(tool, {
session: currentSession,
workspaceId,
status,
docDisplayConfig,
notificationService,
onOpenSession,
onNewSession: () => {
if (!currentSession) return;
setCurrentSession(null);
reMountChatContent();
},
onTogglePin: togglePin,
onOpenDoc: (docId: string, sessionId: string) => {
const { workbench } = framework.get(WorkbenchService);
const viewService = framework.get(ViewService);
workbench.open(`/${docId}?sessionId=${sessionId}`, { at: 'active' });
workbench.openSidebar();
viewService.view.activeSidebarTab('chat');
},
onSessionDelete: (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
deleteSession(sessionToDelete).catch(console.error);
},
});
// initial props
if (!chatTool) {
@@ -318,8 +351,10 @@ export const Component = () => {
workspaceId,
confirmModal,
framework,
deleteSession,
status,
reMountChatContent,
notificationService,
]);
useEffect(() => {
@@ -390,7 +425,7 @@ export const Component = () => {
return (
<>
<ViewTitle title="Intelligence" />
<ViewTitle title={t['com.affine.workspaceSubPath.chat']()} />
<ViewIcon icon="ai" />
<ViewHeader>
<div className={styles.chatHeader}>

View File

@@ -6,7 +6,11 @@ import {
type ChatContextValue,
} from '@affine/core/blocksuite/ai/components/ai-chat-content';
import type { ChatStatus } from '@affine/core/blocksuite/ai/components/ai-chat-messages';
import { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
import {
AIChatToolbar,
configureAIChatToolbar,
getOrCreateAIChatToolbar,
} from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
import { createPlaygroundModal } from '@affine/core/blocksuite/ai/components/playground/modal';
import { registerAIAppEffects } from '@affine/core/blocksuite/ai/effects/app';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
@@ -31,6 +35,7 @@ import type {
CopilotChatHistoryFragment,
UpdateChatSessionInput,
} from '@affine/graphql';
import { useI18n } from '@affine/i18n';
import { RefNodeSlotsProvider } from '@blocksuite/affine/inlines/reference';
import { DocModeProvider } from '@blocksuite/affine/shared/services';
import { createSignalFromObservable } from '@blocksuite/affine/shared/utils';
@@ -40,6 +45,7 @@ import { useFramework, useService } from '@toeverything/infra';
import { html } from 'lit';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createSessionDeleteHandler } from '../../chat-panel-utils';
import * as styles from './chat.css';
import {
resolveInitialSession,
@@ -56,6 +62,7 @@ export interface SidebarTabProps {
export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
const framework = useFramework();
const workbench = useService(WorkbenchService).workbench;
const t = useI18n();
const { closeConfirmModal, openConfirmModal } = useConfirmModal();
const notificationService = useMemo(
@@ -230,30 +237,24 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
[doc, openSession, session?.pinned, session?.sessionId, workbench]
);
const deleteSession = useCallback(
async (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
if (!AIProvider.histories) {
return;
}
const confirm = await 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(
sessionToDelete.workspaceId,
sessionToDelete.docId || undefined,
[sessionToDelete.sessionId]
);
if (sessionToDelete.sessionId === session?.sessionId) {
newSession();
}
}
},
[newSession, notificationService, session?.sessionId]
const deleteSession = useMemo(
() =>
createSessionDeleteHandler({
t,
notificationService,
canDeleteSession: () => Boolean(AIProvider.histories),
cleanupSession: async sessionToDelete => {
await AIProvider.histories?.cleanup(
sessionToDelete.workspaceId,
sessionToDelete.docId || undefined,
[sessionToDelete.sessionId]
);
},
isActiveSession: sessionToDelete =>
sessionToDelete.sessionId === session?.sessionId,
onActiveSessionDeleted: newSession,
}),
[newSession, notificationService, session?.sessionId, t]
);
const togglePin = useCallback(async () => {
@@ -460,31 +461,26 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
return;
}
let tool = chatToolbar;
if (!tool) {
tool = new AIChatToolbar();
}
tool.session = session;
tool.workspaceId = doc.workspace.id;
tool.docId = doc.id;
tool.status = status;
tool.docDisplayConfig = docDisplayConfig;
tool.notificationService = notificationService;
tool.onNewSession = newSession;
tool.onTogglePin = togglePin;
tool.onOpenSession = (sessionId: string) => {
openSession(sessionId).catch(console.error);
};
tool.onOpenDoc = (docId: string, sessionId: string) => {
openDoc(docId, sessionId).catch(console.error);
};
tool.onSessionDelete = (
sessionToDelete: BlockSuitePresets.AIRecentSession
) => {
deleteSession(sessionToDelete).catch(console.error);
};
const tool = getOrCreateAIChatToolbar(chatToolbar);
configureAIChatToolbar(tool, {
session,
workspaceId: doc.workspace.id,
docId: doc.id,
status,
docDisplayConfig,
notificationService,
onNewSession: newSession,
onTogglePin: togglePin,
onOpenSession: (sessionId: string) => {
openSession(sessionId).catch(console.error);
},
onOpenDoc: (docId: string, sessionId: string) => {
openDoc(docId, sessionId).catch(console.error);
},
onSessionDelete: (sessionToDelete: BlockSuitePresets.AIRecentSession) => {
deleteSession(sessionToDelete).catch(console.error);
},
});
if (!chatToolbar) {
chatToolbarContainerRef.current.append(tool);
@@ -618,7 +614,7 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
<div className={styles.loading}>
<Logo1Icon className={styles.loadingIcon} />
<div className={styles.loadingTitle}>
AFFiNE AI is loading history...
{t['com.affine.ai.chat-panel.loading-history']()}
</div>
</div>
</div>
@@ -628,10 +624,13 @@ export const EditorChatPanel = ({ editor, onLoad }: SidebarTabProps) => {
<div className={styles.title}>
{isEmbedding ? (
<span data-testid="chat-panel-embedding-progress">
Embedding {done}/{total}
{t.t('com.affine.ai.chat-panel.embedding-progress', {
done,
total,
})}
</span>
) : (
'AFFiNE AI'
t['com.affine.ai.chat-panel.title']()
)}
</div>
{playgroundVisible ? (

View File

@@ -1,26 +1,26 @@
{
"ar": 99,
"ar": 98,
"ca": 100,
"da": 4,
"de": 100,
"el-GR": 99,
"de": 99,
"el-GR": 98,
"en": 100,
"es-AR": 99,
"es-AR": 98,
"es-CL": 100,
"es": 99,
"fa": 99,
"es": 98,
"fa": 98,
"fr": 100,
"hi": 2,
"it-IT": 100,
"it": 1,
"ja": 99,
"ko": 100,
"nb-NO": 49,
"ja": 98,
"ko": 99,
"nb-NO": 48,
"pl": 100,
"pt-BR": 99,
"pt-BR": 98,
"ru": 100,
"sv-SE": 99,
"uk": 99,
"uk": 98,
"ur": 2,
"zh-Hans": 100,
"zh-Hant": 99

View File

@@ -867,6 +867,37 @@ export function useAFFiNEI18N(): {
* `Failed to insert template, please try again.`
*/
["com.affine.ai.template-insert.failed"](): string;
/**
* `AFFiNE AI`
*/
["com.affine.ai.chat-panel.title"](): string;
/**
* `AFFiNE AI is loading history...`
*/
["com.affine.ai.chat-panel.loading-history"](): string;
/**
* `Embedding {{done}}/{{total}}`
*/
["com.affine.ai.chat-panel.embedding-progress"](options: Readonly<{
done: string;
total: string;
}>): string;
/**
* `Delete this history?`
*/
["com.affine.ai.chat-panel.session.delete.confirm.title"](): string;
/**
* `Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.`
*/
["com.affine.ai.chat-panel.session.delete.confirm.message"](): string;
/**
* `History deleted`
*/
["com.affine.ai.chat-panel.session.delete.toast.success"](): string;
/**
* `Failed to delete history`
*/
["com.affine.ai.chat-panel.session.delete.toast.failed"](): string;
/**
* `All docs`
*/
@@ -7246,6 +7277,10 @@ export function useAFFiNEI18N(): {
* `All docs`
*/
["com.affine.workspaceSubPath.all"](): string;
/**
* `Intelligence`
*/
["com.affine.workspaceSubPath.chat"](): string;
/**
* `Trash`
*/

View File

@@ -206,6 +206,13 @@
"com.affine.ai.login-required.dialog-content": "To use AFFiNE AI, please sign in to your AFFiNE Cloud account.",
"com.affine.ai.login-required.dialog-title": "Sign in to continue",
"com.affine.ai.template-insert.failed": "Failed to insert template, please try again.",
"com.affine.ai.chat-panel.title": "AFFiNE AI",
"com.affine.ai.chat-panel.loading-history": "AFFiNE AI is loading history...",
"com.affine.ai.chat-panel.embedding-progress": "Embedding {{done}}/{{total}}",
"com.affine.ai.chat-panel.session.delete.confirm.title": "Delete this history?",
"com.affine.ai.chat-panel.session.delete.confirm.message": "Do you want to delete this AI conversation history? Once deleted, it cannot be recovered.",
"com.affine.ai.chat-panel.session.delete.toast.success": "History deleted",
"com.affine.ai.chat-panel.session.delete.toast.failed": "Failed to delete history",
"com.affine.all-pages.header": "All docs",
"com.affine.app-sidebar.learn-more": "Learn more",
"com.affine.app-sidebar.star-us": "Star us",
@@ -1813,6 +1820,7 @@
"com.affine.workspaceList.workspaceListType.local": "Local storage",
"com.affine.workspaceList.addServer": "Add Server",
"com.affine.workspaceSubPath.all": "All docs",
"com.affine.workspaceSubPath.chat": "Intelligence",
"com.affine.workspaceSubPath.trash": "Trash",
"com.affine.workspaceSubPath.trash.empty-description": "Deleted docs will appear here.",
"com.affine.write_with_a_blank_page": "Write with a blank page",

View File

@@ -206,6 +206,13 @@
"com.affine.ai.login-required.dialog-content": "要使用 AFFiNE AI请先登录您的 AFFiNE Cloud 帐户。",
"com.affine.ai.login-required.dialog-title": "登录以继续",
"com.affine.ai.template-insert.failed": "插入模板失败,请重试。",
"com.affine.ai.chat-panel.title": "AFFiNE AI",
"com.affine.ai.chat-panel.loading-history": "AFFiNE AI 正在加载历史记录...",
"com.affine.ai.chat-panel.embedding-progress": "嵌入 {{done}}/{{total}}",
"com.affine.ai.chat-panel.session.delete.confirm.title": "删除此历史记录?",
"com.affine.ai.chat-panel.session.delete.confirm.message": "确定要删除这段 AI 对话历史记录吗?删除后无法恢复。",
"com.affine.ai.chat-panel.session.delete.toast.success": "已删除历史记录",
"com.affine.ai.chat-panel.session.delete.toast.failed": "删除历史记录失败",
"com.affine.all-pages.header": "所有文档",
"com.affine.app-sidebar.learn-more": "了解更多",
"com.affine.app-sidebar.star-us": "给我们点亮星标",
@@ -1808,6 +1815,7 @@
"com.affine.workspaceList.workspaceListType.local": "本地储存",
"com.affine.workspaceList.addServer": "添加服务器",
"com.affine.workspaceSubPath.all": "全部文档",
"com.affine.workspaceSubPath.chat": "Intelligence",
"com.affine.workspaceSubPath.trash": "回收站",
"com.affine.workspaceSubPath.trash.empty-description": "已删除的文档将显示在此处。",
"com.affine.write_with_a_blank_page": "在空白页面书写",

View File

@@ -205,6 +205,13 @@
"com.affine.ai.login-required.dialog-content": "要使用 AFFiNE AI請先登入您的 AFFiNE Cloud 帳戶。",
"com.affine.ai.login-required.dialog-title": "登入以繼續",
"com.affine.ai.template-insert.failed": "插入模板失敗,請重試。",
"com.affine.ai.chat-panel.title": "AFFiNE AI",
"com.affine.ai.chat-panel.loading-history": "AFFiNE AI 正在載入歷史記錄...",
"com.affine.ai.chat-panel.embedding-progress": "嵌入 {{done}}/{{total}}",
"com.affine.ai.chat-panel.session.delete.confirm.title": "刪除此歷史記錄?",
"com.affine.ai.chat-panel.session.delete.confirm.message": "確定要刪除這段 AI 對話歷史記錄嗎?刪除後無法復原。",
"com.affine.ai.chat-panel.session.delete.toast.success": "已刪除歷史記錄",
"com.affine.ai.chat-panel.session.delete.toast.failed": "刪除歷史記錄失敗",
"com.affine.all-pages.header": "所有文件",
"com.affine.app-sidebar.learn-more": "了解更多",
"com.affine.app-sidebar.star-us": "給我們點亮星標",
@@ -1785,6 +1792,7 @@
"com.affine.workspaceList.workspaceListType.local": "本地儲存",
"com.affine.workspaceList.addServer": "新增伺服器",
"com.affine.workspaceSubPath.all": "所有頁面",
"com.affine.workspaceSubPath.chat": "Intelligence",
"com.affine.workspaceSubPath.trash": "廢紙簍",
"com.affine.workspaceSubPath.trash.empty-description": "已刪除的文件將顯示在此處。",
"com.affine.write_with_a_blank_page": "在空白頁書寫",