From fe00293e3ec2ff038ac41c4577ea0b53513f4fe0 Mon Sep 17 00:00:00 2001 From: Wu Yue Date: Thu, 10 Jul 2025 12:25:12 +0800 Subject: [PATCH] feat(core): disable pin chat while generating AI answers (#13131) Close [AI-316](https://linear.app/affine-design/issue/AI-316) ## Summary by CodeRabbit * **New Features** * Chat status is now displayed and updated in the chat panel and toolbar, allowing users to see when the chat is generating a response. * The pin button in the chat toolbar is disabled while the chat is generating a response, preventing pin actions during this time and providing feedback via a notification if attempted. --- .../src/blocksuite/ai/chat-panel/index.ts | 6 ++++ .../ai-chat-toolbar/ai-chat-toolbar.ts | 28 ++++++++++++++++++- .../desktop/pages/workspace/chat/index.tsx | 15 +++++++++- 3 files changed, 47 insertions(+), 2 deletions(-) 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 3b34c3912..2f50d0aeb 100644 --- a/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts +++ b/packages/frontend/core/src/blocksuite/ai/chat-panel/index.ts @@ -30,6 +30,7 @@ import type { AIPlaygroundConfig, AIReasoningConfig, } from '../components/ai-chat-input'; +import type { ChatStatus } from '../components/ai-chat-messages'; import { createPlaygroundModal } from '../components/playground/modal'; import { AIProvider } from '../provider'; import type { AppSidebarConfig } from './chat-config'; @@ -138,6 +139,9 @@ export class ChatPanel extends SignalWatcher( @state() accessor embeddingProgress: [number, number] = [0, 0]; + @state() + accessor status: ChatStatus = 'idle'; + private isSidebarOpen: Signal = signal(false); private sidebarWidth: Signal = signal(undefined); @@ -171,6 +175,7 @@ export class ChatPanel extends SignalWatcher( .session=${this.session} .workspaceId=${this.doc.workspace.id} .docId=${this.doc.id} + .status=${this.status} .onNewSession=${this.newSession} .onTogglePin=${this.togglePin} .onOpenSession=${this.openSession} @@ -359,6 +364,7 @@ export class ChatPanel extends SignalWatcher( private readonly onContextChange = async ( context: Partial ) => { + this.status = context.status ?? 'idle'; if (context.status === 'success') { await this.rebindSession(); } 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 c0b0b7035..c6a5ff8aa 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 @@ -15,6 +15,7 @@ import { css, html } from 'lit'; import { property, query } from 'lit/decorators.js'; import type { DocDisplayConfig } from '../ai-chat-chips'; +import type { ChatStatus } from '../ai-chat-messages'; export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) @@ -26,6 +27,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { @property({ attribute: false }) accessor docId: string | undefined; + @property({ attribute: false }) + accessor status!: ChatStatus; + @property({ attribute: false }) accessor onNewSession!: () => void; @@ -49,6 +53,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { private abortController: AbortController | null = null; + get isGenerating() { + return this.status === 'transmitting' || this.status === 'loading'; + } + static override styles = css` .ai-chat-toolbar { display: flex; @@ -72,6 +80,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { height: 16px; color: ${unsafeCSSVarV2('icon/primary')}; } + + &[data-disabled='true'] { + cursor: not-allowed; + } } } `; @@ -84,7 +96,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { ${PlusIcon()} New Chat -
+
${pinned ? PinedIcon() : PinIcon()} ${pinned ? 'Unpin this Chat' : 'Pin this Chat'} @@ -101,6 +117,16 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) { `; } + private readonly onPinClick = async () => { + if (this.isGenerating) { + this.notificationService.toast( + 'Cannot pin a chat while generating an answer' + ); + return; + } + await this.onTogglePin(); + }; + private readonly unpinConfirm = async () => { if (this.session && this.session.pinned) { try { diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx index f9f5463fc..1295ca54b 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx @@ -1,6 +1,10 @@ import { observeResize, useConfirmModal } from '@affine/component'; import { CopilotClient } from '@affine/core/blocksuite/ai'; -import { AIChatContent } from '@affine/core/blocksuite/ai/components/ai-chat-content'; +import { + AIChatContent, + 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 type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt'; import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service'; @@ -55,6 +59,7 @@ export const Component = () => { const [currentSession, setCurrentSession] = useState( null ); + const [status, setStatus] = useState('idle'); const [isTogglingPin, setIsTogglingPin] = useState(false); const [isOpeningSession, setIsOpeningSession] = useState(false); const chatContainerRef = useRef(null); @@ -129,6 +134,10 @@ export const Component = () => { [chatContent, chatTool, client, isOpeningSession, workspaceId] ); + const onContextChange = useCallback((context: Partial) => { + setStatus(context.status ?? 'idle'); + }, []); + const confirmModal = useConfirmModal(); // init or update ai-chat-content @@ -149,6 +158,7 @@ export const Component = () => { content.searchMenuConfig = searchMenuConfig; content.networkSearchConfig = networkSearchConfig; content.reasoningConfig = reasoningConfig; + content.onContextChange = onContextChange; content.affineFeatureFlagService = framework.get(FeatureFlagService); content.affineWorkspaceDialogService = framework.get( WorkspaceDialogService @@ -180,6 +190,7 @@ export const Component = () => { searchMenuConfig, workspaceId, confirmModal, + onContextChange, ]); // init or update header ai-chat-toolbar @@ -195,6 +206,7 @@ export const Component = () => { tool.session = currentSession; tool.workspaceId = workspaceId; + tool.status = status; tool.docDisplayConfig = docDisplayConfig; tool.onOpenSession = onOpenSession; tool.notificationService = new NotificationServiceImpl( @@ -237,6 +249,7 @@ export const Component = () => { workspaceId, confirmModal, framework, + status, ]); const onChatContainerRef = useCallback((node: HTMLDivElement) => {