feat(core): disable pin chat while generating AI answers (#13131)

Close [AI-316](https://linear.app/affine-design/issue/AI-316)

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

## 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.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-10 12:25:12 +08:00
committed by GitHub
parent 385226083f
commit fe00293e3e
3 changed files with 47 additions and 2 deletions
@@ -30,6 +30,7 @@ import type {
AIPlaygroundConfig, AIPlaygroundConfig,
AIReasoningConfig, AIReasoningConfig,
} from '../components/ai-chat-input'; } from '../components/ai-chat-input';
import type { ChatStatus } from '../components/ai-chat-messages';
import { createPlaygroundModal } from '../components/playground/modal'; import { createPlaygroundModal } from '../components/playground/modal';
import { AIProvider } from '../provider'; import { AIProvider } from '../provider';
import type { AppSidebarConfig } from './chat-config'; import type { AppSidebarConfig } from './chat-config';
@@ -138,6 +139,9 @@ export class ChatPanel extends SignalWatcher(
@state() @state()
accessor embeddingProgress: [number, number] = [0, 0]; accessor embeddingProgress: [number, number] = [0, 0];
@state()
accessor status: ChatStatus = 'idle';
private isSidebarOpen: Signal<boolean | undefined> = signal(false); private isSidebarOpen: Signal<boolean | undefined> = signal(false);
private sidebarWidth: Signal<number | undefined> = signal(undefined); private sidebarWidth: Signal<number | undefined> = signal(undefined);
@@ -171,6 +175,7 @@ export class ChatPanel extends SignalWatcher(
.session=${this.session} .session=${this.session}
.workspaceId=${this.doc.workspace.id} .workspaceId=${this.doc.workspace.id}
.docId=${this.doc.id} .docId=${this.doc.id}
.status=${this.status}
.onNewSession=${this.newSession} .onNewSession=${this.newSession}
.onTogglePin=${this.togglePin} .onTogglePin=${this.togglePin}
.onOpenSession=${this.openSession} .onOpenSession=${this.openSession}
@@ -359,6 +364,7 @@ export class ChatPanel extends SignalWatcher(
private readonly onContextChange = async ( private readonly onContextChange = async (
context: Partial<ChatContextValue> context: Partial<ChatContextValue>
) => { ) => {
this.status = context.status ?? 'idle';
if (context.status === 'success') { if (context.status === 'success') {
await this.rebindSession(); await this.rebindSession();
} }
@@ -15,6 +15,7 @@ import { css, html } from 'lit';
import { property, query } from 'lit/decorators.js'; import { property, query } from 'lit/decorators.js';
import type { DocDisplayConfig } from '../ai-chat-chips'; import type { DocDisplayConfig } from '../ai-chat-chips';
import type { ChatStatus } from '../ai-chat-messages';
export class AIChatToolbar extends WithDisposable(ShadowlessElement) { export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
@@ -26,6 +27,9 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
@property({ attribute: false }) @property({ attribute: false })
accessor docId: string | undefined; accessor docId: string | undefined;
@property({ attribute: false })
accessor status!: ChatStatus;
@property({ attribute: false }) @property({ attribute: false })
accessor onNewSession!: () => void; accessor onNewSession!: () => void;
@@ -49,6 +53,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
private abortController: AbortController | null = null; private abortController: AbortController | null = null;
get isGenerating() {
return this.status === 'transmitting' || this.status === 'loading';
}
static override styles = css` static override styles = css`
.ai-chat-toolbar { .ai-chat-toolbar {
display: flex; display: flex;
@@ -72,6 +80,10 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
height: 16px; height: 16px;
color: ${unsafeCSSVarV2('icon/primary')}; color: ${unsafeCSSVarV2('icon/primary')};
} }
&[data-disabled='true'] {
cursor: not-allowed;
}
} }
} }
`; `;
@@ -84,7 +96,11 @@ export class AIChatToolbar extends WithDisposable(ShadowlessElement) {
${PlusIcon()} ${PlusIcon()}
<affine-tooltip>New Chat</affine-tooltip> <affine-tooltip>New Chat</affine-tooltip>
</div> </div>
<div class="chat-toolbar-icon" @click=${this.onTogglePin}> <div
class="chat-toolbar-icon"
@click=${this.onPinClick}
data-disabled=${this.isGenerating}
>
${pinned ? PinedIcon() : PinIcon()} ${pinned ? PinedIcon() : PinIcon()}
<affine-tooltip> <affine-tooltip>
${pinned ? 'Unpin this Chat' : 'Pin this Chat'} ${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 () => { private readonly unpinConfirm = async () => {
if (this.session && this.session.pinned) { if (this.session && this.session.pinned) {
try { try {
@@ -1,6 +1,10 @@
import { observeResize, useConfirmModal } from '@affine/component'; import { observeResize, useConfirmModal } from '@affine/component';
import { CopilotClient } from '@affine/core/blocksuite/ai'; 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 { AIChatToolbar } from '@affine/core/blocksuite/ai/components/ai-chat-toolbar';
import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt'; import type { PromptKey } from '@affine/core/blocksuite/ai/provider/prompt';
import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service'; import { NotificationServiceImpl } from '@affine/core/blocksuite/view-extensions/editor-view/notification-service';
@@ -55,6 +59,7 @@ export const Component = () => {
const [currentSession, setCurrentSession] = useState<CopilotSession | null>( const [currentSession, setCurrentSession] = useState<CopilotSession | null>(
null null
); );
const [status, setStatus] = useState<ChatStatus>('idle');
const [isTogglingPin, setIsTogglingPin] = useState(false); const [isTogglingPin, setIsTogglingPin] = useState(false);
const [isOpeningSession, setIsOpeningSession] = useState(false); const [isOpeningSession, setIsOpeningSession] = useState(false);
const chatContainerRef = useRef<HTMLDivElement>(null); const chatContainerRef = useRef<HTMLDivElement>(null);
@@ -129,6 +134,10 @@ export const Component = () => {
[chatContent, chatTool, client, isOpeningSession, workspaceId] [chatContent, chatTool, client, isOpeningSession, workspaceId]
); );
const onContextChange = useCallback((context: Partial<ChatContextValue>) => {
setStatus(context.status ?? 'idle');
}, []);
const confirmModal = useConfirmModal(); const confirmModal = useConfirmModal();
// init or update ai-chat-content // init or update ai-chat-content
@@ -149,6 +158,7 @@ export const Component = () => {
content.searchMenuConfig = searchMenuConfig; content.searchMenuConfig = searchMenuConfig;
content.networkSearchConfig = networkSearchConfig; content.networkSearchConfig = networkSearchConfig;
content.reasoningConfig = reasoningConfig; content.reasoningConfig = reasoningConfig;
content.onContextChange = onContextChange;
content.affineFeatureFlagService = framework.get(FeatureFlagService); content.affineFeatureFlagService = framework.get(FeatureFlagService);
content.affineWorkspaceDialogService = framework.get( content.affineWorkspaceDialogService = framework.get(
WorkspaceDialogService WorkspaceDialogService
@@ -180,6 +190,7 @@ export const Component = () => {
searchMenuConfig, searchMenuConfig,
workspaceId, workspaceId,
confirmModal, confirmModal,
onContextChange,
]); ]);
// init or update header ai-chat-toolbar // init or update header ai-chat-toolbar
@@ -195,6 +206,7 @@ export const Component = () => {
tool.session = currentSession; tool.session = currentSession;
tool.workspaceId = workspaceId; tool.workspaceId = workspaceId;
tool.status = status;
tool.docDisplayConfig = docDisplayConfig; tool.docDisplayConfig = docDisplayConfig;
tool.onOpenSession = onOpenSession; tool.onOpenSession = onOpenSession;
tool.notificationService = new NotificationServiceImpl( tool.notificationService = new NotificationServiceImpl(
@@ -237,6 +249,7 @@ export const Component = () => {
workspaceId, workspaceId,
confirmModal, confirmModal,
framework, framework,
status,
]); ]);
const onChatContainerRef = useCallback((node: HTMLDivElement) => { const onChatContainerRef = useCallback((node: HTMLDivElement) => {